diff --git a/.dockerignore b/.dockerignore index 3729ff0c..af6e61c9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,7 +13,7 @@ **/*.jfm **/azds.yaml **/bin -**/charts +charts **/docker-compose* **/Dockerfile* **/node_modules diff --git a/.github/workflows/bitween-api-cicd-gateway.yml b/.github/workflows/bitween-api-cicd-gateway.yml index 421f4ceb..605a872d 100644 --- a/.github/workflows/bitween-api-cicd-gateway.yml +++ b/.github/workflows/bitween-api-cicd-gateway.yml @@ -1,20 +1,29 @@ name: Bitween API CI/CD (Gateway · both) run-name: Run-${{ github.ref_name }}-${{ github.run_number }} +# Two pipelines that publish nothing under the same name: +# releases/r10.0-develop the simplify9/bitween-develop image and the bitween-develop chart, +# deployed to bitween-dev on the dev cluster (app-dev.bitween.io). No NuGet +# packages, the CLI as a pre-release. +# releases/r10.0 the simplify9/bitween image and the bitween chart clients install +# (ChartMuseum + GHCR), the NuGet packages, the CLI and the SDKs, deployed +# to bitween on the production cluster (app.bitween.io). +# Run workflow by hand on either branch to rerun that branch's pipeline. on: push: branches: - - releases/** + - releases/r10.0 + - releases/r10.0-develop workflow_dispatch: -# One queue for staging AND release, not one per branch. Both jobs below take their patch from -# the same 10.0.x git tags and push the same docker.io/simplify9/bitween:10.0. image tag, -# and a run only claims its patch when it tags at the very end. Two runs overlapping (a staging -# push, then the staging -> r10.0 merge seconds later) would compute the same patch: the image -# tag is overwritten by whichever finishes last, while ChartMuseum (409 = success) and NuGet -# (--skip-duplicate) silently keep whichever came first. +# One queue per branch. Both pipelines still take their patch from the same 10.0.x git tags, so a +# develop run and a release run that overlap can compute the same patch, but they publish it under +# different names (bitween-develop / bitween, cli-v10.0.N-dev. / cli-v10.0.N, git tag +# 10.0.N-releases-r10-0-develop. / 10.0.N), so nothing is overwritten or silently kept. One +# queue for both would be worse: GitHub keeps a single pending run per group, so a develop push +# would cancel a release waiting behind another develop run. concurrency: - group: cicd-gateway-bitween-10.0 + group: cicd-gateway-bitween-${{ github.ref_name }} cancel-in-progress: false # contents: write -> tag job. packages: write -> publish AND pull the Helm chart @@ -27,8 +36,8 @@ permissions: jobs: build-publish-deploy: - # Staging: build, publish to GHCR only, deploy to playground (or any branch, manually). - if: ${{ (github.event_name == 'push' && github.ref_name == 'releases/r10.0-staging') || github.event_name == 'workflow_dispatch' }} + # Develop: build, publish bitween-develop, deploy to bitween-dev. + if: ${{ github.ref_name == 'releases/r10.0-develop' }} uses: simplify9/.github/.github/workflows/reusable-service-cicd.yml@main with: major-version: '10' @@ -39,62 +48,78 @@ jobs: docker-context: '.' docker-platforms: 'linux/amd64' - chart-name: 'bitween' # must match charts/default Chart.yaml name: + # The production chart, published as bitween-develop: rename-chart sets Chart.yaml's name + # at package time, so develop and production ship from one chart directory and cannot drift. + chart-name: 'bitween-develop' chart-path: './charts/default' + rename-chart: true + # ChartMuseum to install it by hand, GHCR for the deploy below. + chart-publish-method: 'both' + chart-repo-url: 'https://charts.sf9.io' - # 1) Publish the Helm chart to GHCR OCI only. ChartMuseum is what clients install from, - # so only the release job below publishes there; staging versions are plain x.y.z and - # would otherwise be the newest chart a client gets. - chart-publish-method: 'github-oci' - - # Docker image stays on Docker Hub + # The chart's image is /:, so the image is named after + # the chart. Its own repository also keeps develop off simplify9/bitween:latest. container-registry: 'docker.io' - image-name: 'simplify9/bitween' + image-name: 'simplify9/bitween-develop' - # Build + unit-test + push the SDK NuGet package (versioned with the - # pipeline semver, e.g. 8.1.x — was 8.0. in the old job). - nuget-projects: 'SW.Bitween.Sdk/SW.Bitween.Sdk.csproj' + # No nuget-projects: develop publishes no packages. The tests still run (build and test, + # no pack); as before, a failure does not hold back the deploy. run-tests: 'true' - # Integration tests run against real Postgres/RabbitMQ/MailHog via - # Testcontainers — self-contained, no extra CI services needed, since - # ubuntu-latest already has a Docker daemon available. test-projects: | SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj - # 2) Deploy to playground via Gateway API + # Deploy to bitween-dev via Gateway API deploy: true - # 3) Pull the chart from GHCR OCI + # Pull the chart from GHCR OCI deploy-chart-source: 'github-oci' - deploy-namespace: 'playground' + deploy-namespace: 'bitween-dev' + # Release bitween with nameOverride=bitween (helm-set-values), so every resource name and + # selector is what the bitween chart renders in production. Gateway API gives a hostname to + # its oldest HTTPRoute: any other release serving app-dev.bitween.io on this cluster keeps + # the host until it is uninstalled. + deploy-release-name: 'bitween' routing-mode: 'gateway-api' gateway-hostnames: 'app-dev.bitween.io' # '/' serves the admin SPA out of this image's wwwroot, so the root has to route - # here too — previously only /api and /swagger did, leaving the root on the retired + # here too - previously only /api and /swagger did, leaving the root on the retired # legacy Bitween-UI deployment and making every deploy look like the UI never changed. gateway-paths: '/,/api,/swagger' - # app-dev.bitween.io is not *.sf9.io, so the deploy auto-onboards a - # dedicated listener + cert-manager Certificate on the parent gateway - # (public-gateway · s9-dev-edge · cilium · letsencrypt-production-gateway). - - # Non-routing chart values (routing itself is generated by gateway-api mode). + # Attach to the dev gateway's *.bitween.io wildcard listener (public-gateway · s9-dev-edge), + # certificate included, instead of onboarding a listener and certificate for the host. The + # deploy fails before helm runs if the listener is missing. An app-dev.bitween.io listener on + # that gateway would take the host from the wildcard and answer 404, since this route no + # longer attaches to it. + gateway-section-name: 'https-wildcard-bitween-io' + + # Non-routing chart values (routing itself is generated by gateway-api mode). Every line is + # handed to helm --set as it stands, so no comments inside. replicaCount=2: the lab's phase 6 + # kills one mid-traffic, and anything that runs on every replica at once (scheduled jobs, data + # source leases) shows up here first. + # AWS_*_CHECKSUM_*=when_required: an experiment for S3-compatible stores. Since AWSSDK.S3 3.7.410 + # uploads are aws-chunked with a trailing checksum, which such stores save into the object + # (the lab's S3 invoices arrive starting "C3;chunk-signature=..."). The lasting fix belongs + # in SW-CloudFiles; this shows on staging whether the SDK default is the cause. helm-set-values: | + nameOverride=bitween global.environment=Development + replicaCount=2 resources.requests.cpu=25m resources.requests.memory=704Mi resources.limits.memory=1408Mi + environmentVariables.Bitween__BusProvidersEnabled=true + environmentVariables.AWS_REQUEST_CHECKSUM_CALCULATION=when_required + environmentVariables.AWS_RESPONSE_CHECKSUM_VALIDATION=when_required secrets: # Docker image push credentials (Docker Hub) registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_TOKEN }} + chartmuseum-username: ${{ secrets.CM_USER }} + chartmuseum-password: ${{ secrets.CM_PASSWORD }} # GHCR chart push + pull and git tagging use the built-in token github-token: ${{ secrets.GITHUB_TOKEN }} - # NuGet API key for the SDK package push (publishes to nuget.org, the - # reusable's default feed — no nuget-source override needed). - nuget-api-key: ${{ secrets.SWNUGETKEY }} - # Gateway-API cluster kubeconfig (V2) kubeconfig-gateway: ${{ secrets.S9DEV_KUBECONFIG_V2 }} @@ -103,17 +128,17 @@ jobs: # App configuration passed to helm --set-string during deploy (masked) # # global.token.key signs every authentication token. The chart still carries a sample - # key as its default, and that value is published in this public repository — anyone + # key as its default, and that value is published in this public repository - anyone # holding it can mint a token for any identity, so the app refuses to start on it. # Set the TokenKey secret to a long random string unique to this environment # (openssl rand -base64 48). Changing it signs out everyone holding an older token. helm-set-secret-values: 'global.token.key=${{ secrets.TokenKey }},db=${{ secrets.dbConnection }},global.bus.rabbitUrl=${{ secrets.rabbitUrl }},global.cloudFiles.secretAccessKey=${{ secrets.SecretAccessKey }},global.cloudFiles.accessKeyId=${{ secrets.AccessKeyId }},global.cloudFiles.serviceUrl=${{ secrets.ServiceUrl }},global.cloudFiles.bucketName=${{ secrets.BucketName }},environmentVariables.Bitween__RabbitMqManagementUrl=${{ secrets.RabbitMqManagementUrl }},environmentVariables.Bitween__RabbitMqManagementPassword=${{ secrets.RabbitMqManagementPassword }},environmentVariables.Bitween__RabbitMqManagementUsername=${{ secrets.RabbitMqManagementUsername }}' publish-release: - # Release: build and publish to ChartMuseum for clients, never deploy. releases/r10.0 is the + # Release: build and publish for clients, deploy production. releases/r10.0 is the # repository's default branch, so the reusable workflow treats it as a release: a clean # 10.0. git tag, and the critical Dependabot gate runs first. - if: ${{ github.event_name == 'push' && github.ref_name == 'releases/r10.0' }} + if: ${{ github.ref_name == 'releases/r10.0' }} uses: simplify9/.github/.github/workflows/reusable-service-cicd.yml@main with: major-version: '10' @@ -126,19 +151,54 @@ jobs: chart-name: 'bitween' chart-path: './charts/default' - chart-publish-method: 'chartmuseum' + # ChartMuseum, where clients install from today, and GHCR OCI (oci://ghcr.io/simplify9/charts/bitween), + # the same versions as an immutable, digest-pinned mirror. + chart-publish-method: 'both' chart-repo-url: 'https://charts.sf9.io' container-registry: 'docker.io' image-name: 'simplify9/bitween' - nuget-projects: 'SW.Bitween.Sdk/SW.Bitween.Sdk.csproj' + # The SDK and the adapter contract package (SimplyWorks.Bitween.Adapters) adapters reference. + nuget-projects: >- + SW.Bitween.Sdk/SW.Bitween.Sdk.csproj + SW.Bitween.Adapters/SW.Bitween.Adapters.csproj run-tests: 'true' + # Integration tests run against real Postgres/RabbitMQ/MailHog via + # Testcontainers - self-contained, no extra CI services needed, since + # ubuntu-latest already has a Docker daemon available. test-projects: | SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj - # deploy is left at its default, false: the deploy job never runs, so no kubeconfig - # or app secrets are passed. + # Deploy to bitween on the production cluster. Off until the repository variable + # BITWEEN_PROD_DEPLOY is 'true': first the PROD_* secrets below must exist, + # S9PROD_KUBECONFIG_V2 must be shared with this repository, and app.bitween.io must + # point at the production gateway. Without them a deploy installs a release with no + # database, or fails waiting for its certificate. + deploy: ${{ vars.BITWEEN_PROD_DEPLOY == 'true' }} + deploy-chart-source: 'github-oci' + deploy-namespace: 'bitween' + deploy-environment: 'production' + routing-mode: 'gateway-api' + gateway-hostnames: 'app.bitween.io' + gateway-paths: '/,/api,/swagger' + # The production cluster's gateway. The default, s9-dev-edge, does not exist there, and + # the deploy would create a second public-gateway in it instead of failing. + gateway-parent-namespace: 's9-prod-edge' + # The production gateway's *.bitween.io wildcard listener, as on develop. + gateway-section-name: 'https-wildcard-bitween-io' + # Every line is handed to helm --set as it stands, so no comments inside. + # AWS_*_CHECKSUM_*=when_required: production stores files in DigitalOcean Spaces too, so it + # carries develop's checksum setting (see the note there) until the fix lands in SW-CloudFiles. + helm-set-values: | + global.environment=Production + replicaCount=2 + resources.requests.cpu=25m + resources.requests.memory=704Mi + resources.limits.memory=1408Mi + environmentVariables.Bitween__BusProvidersEnabled=true + environmentVariables.AWS_REQUEST_CHECKSUM_CALCULATION=when_required + environmentVariables.AWS_RESPONSE_CHECKSUM_VALIDATION=when_required secrets: registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -147,3 +207,37 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} nuget-api-key: ${{ secrets.SWNUGETKEY }} dependabot-alerts-token: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }} + + # Production cluster kubeconfig (V2, Gateway API) + kubeconfig-gateway: ${{ secrets.S9PROD_KUBECONFIG_V2 }} + + # Production's own configuration, the same settings as the develop job's above. PROD_TOKENKEY + # is its own random string (openssl rand -base64 48), never the develop key. + helm-set-secret-values: 'global.token.key=${{ secrets.PROD_TOKENKEY }},db=${{ secrets.PROD_DBCONNECTION }},global.bus.rabbitUrl=${{ secrets.PROD_RABBITURL }},global.cloudFiles.secretAccessKey=${{ secrets.PROD_SECRETACCESSKEY }},global.cloudFiles.accessKeyId=${{ secrets.PROD_ACCESSKEYID }},global.cloudFiles.serviceUrl=${{ secrets.PROD_SERVICEURL }},global.cloudFiles.bucketName=${{ secrets.PROD_BUCKETNAME }},environmentVariables.Bitween__RabbitMqManagementUrl=${{ secrets.PROD_RABBITMQMANAGEMENTURL }},environmentVariables.Bitween__RabbitMqManagementPassword=${{ secrets.PROD_RABBITMQMANAGEMENTPASSWORD }},environmentVariables.Bitween__RabbitMqManagementUsername=${{ secrets.PROD_RABBITMQMANAGEMENTUSERNAME }}' + + # The bitween CLI and the SDKs carry the version the run above computed, so nothing is tagged by + # hand. A develop build releases the CLI as a pre-release, cli-v-dev.; the release + # from releases/r10.0 releases it as the latest, cli-v, and publishes the SDKs. Each runs + # only when the job it needs ran, so the branch is decided above. + cli-develop: + needs: build-publish-deploy + uses: ./.github/workflows/cli-release.yml + with: + version: ${{ needs.build-publish-deploy.outputs.version }} + prerelease: true + run-number: ${{ github.run_number }} + + cli-release: + needs: publish-release + uses: ./.github/workflows/cli-release.yml + with: + version: ${{ needs.publish-release.outputs.version }} + + sdk-release: + needs: publish-release + permissions: + contents: read + uses: ./.github/workflows/sdk-release.yml + with: + version: ${{ needs.publish-release.outputs.version }} + secrets: inherit diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml new file mode 100644 index 00000000..24496388 --- /dev/null +++ b/.github/workflows/cli-release.yml @@ -0,0 +1,143 @@ +name: bitween CLI release + +# Publishes the bitween command as self-contained binaries on a GitHub release: nothing needs .NET +# installed to run them. Never run by hand or by a tag: the CI run that publishes this repository +# calls it with the version it computed (determine-semver: the next patch from the git tags), so the +# CLI carries the version of the release it ships with. The computed version is the bare x.y.z on +# develop too, so a develop build says so with prerelease and passes its run number. +# a release, 10.0.61 cli-v10.0.61 the latest release +# a prerelease, 10.0.61 with prerelease and run 121 cli-v10.0.61-dev.121 a pre-release, to try out +# Called by bitween-api-cicd-gateway.yml: after the release from releases/r10.0, and after each develop build. +on: + workflow_call: + inputs: + version: + description: "The version the calling run computed" + type: string + required: true + prerelease: + description: "Release as a pre-release, named after the run" + type: boolean + default: false + run-number: + description: "The calling run's number, for a pre-release" + type: string + default: "" + +permissions: + contents: write + +jobs: + channel: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.channel.outputs.version }} + prerelease: ${{ steps.channel.outputs.prerelease }} + steps: + - name: Channel + id: channel + env: + VERSION: ${{ inputs.version }} + PRERELEASE: ${{ inputs.prerelease }} + RUN_NUMBER: ${{ inputs.run-number }} + run: | + v="$VERSION" + if [ "$PRERELEASE" = "true" ]; then + # A pre-release names its run: 10.0.61 from run 121 is 10.0.61-dev.121. + v="${v%%-*}-dev.${RUN_NUMBER:-${GITHUB_RUN_NUMBER}}" + echo "prerelease=true" >> "$GITHUB_OUTPUT" + elif [[ "$v" == *-* ]]; then + # A version that already carries its branch and run: 10.0.61-releases-r10-0-develop.121 is 10.0.61-dev.121. + v="${v%%-*}-dev.${v##*.}" + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + echo "version=$v" >> "$GITHUB_OUTPUT" + + build: + needs: channel + runs-on: ubuntu-latest + strategy: + matrix: + rid: [linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64, osx-x64, osx-arm64, win-x64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + - name: Publish + run: > + dotnet publish SW.Bitween.Cli -c Release -r ${{ matrix.rid }} --self-contained + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=none + -p:Version=${{ needs.channel.outputs.version }} -o out + - name: Package + run: | + mkdir -p dist + cp docs/cli.md out/README.md 2>/dev/null || true + if [[ "${{ matrix.rid }}" == win-* ]]; then + (cd out && zip -q -r ../dist/bitween-${{ matrix.rid }}.zip .) + else + tar -czf dist/bitween-${{ matrix.rid }}.tar.gz -C out . + fi + - uses: actions/upload-artifact@v4 + with: + name: bitween-${{ matrix.rid }} + path: dist/* + + # The data source providers (RabbitMQ, SQS and the database engines) as adapter packages, with the + # release's version: installed like any adapter, with bitween adapter publish or apply's adapters + # section. Nothing else publishes them, and a Bitween runs no data source without them. + providers: + needs: channel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + - name: Build + env: + VERSION: ${{ needs.channel.outputs.version }} + run: | + dotnet build SW.Bitween.Cli -c Release -o cli + mkdir -p dist + for project in SW.Bitween.Adapters.Bus.RabbitMq SW.Bitween.Adapters.Bus.Sqs SW.Bitween.Adapters.Db.PostgreSql \ + SW.Bitween.Adapters.Db.MySql SW.Bitween.Adapters.Db.SqlServer SW.Bitween.Adapters.Db.Oracle; do + jq --arg v "$VERSION" '.version = $v' "$project/adapter.json" > adapter.json.tmp && mv adapter.json.tmp "$project/adapter.json" + dotnet cli/bitween.dll adapter build "$project" -o dist + done + ls -la dist + - uses: actions/upload-artifact@v4 + with: + name: bitween-providers + path: dist/*.zip + + release: + needs: [channel, build, providers] + runs-on: ubuntu-latest + steps: + # Only the CLI builds' own artifacts. Without a pattern this fetched every artifact in the run, + # including the build record docker/build-push-action uploads in the deploy job, which can't be + # extracted and failed the release (and would have landed in SHA256SUMS if it could). + - uses: actions/download-artifact@v4 + with: + pattern: bitween-* + path: dist + merge-multiple: true + - name: Checksums + run: cd dist && sha256sum * > SHA256SUMS && cat SHA256SUMS + - name: Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + v="${{ needs.channel.outputs.version }}" + install="curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/releases/r10.0/scripts/install-cli.sh" + if [ "${{ needs.channel.outputs.prerelease }}" = "true" ]; then + gh release create "cli-v$v" dist/* --title "bitween CLI $v (pre-release)" --target "${{ github.sha }}" --prerelease \ + --notes "A pre-release of the bitween command, built from ${{ github.ref_name }}, to try out. Install: $install | BITWEEN_CLI_VERSION=$v sh - or download the archive for your platform and check it against SHA256SUMS." + else + gh release create "cli-v$v" dist/* --title "bitween CLI $v" --target "${{ github.sha }}" --latest \ + --notes "The bitween command, self-contained for each platform. Install: $install | sh - or download the archive for your platform and check it against SHA256SUMS." + fi diff --git a/.github/workflows/critical-vuln-check.yml b/.github/workflows/critical-vuln-check.yml index 70f912b7..cab88580 100644 --- a/.github/workflows/critical-vuln-check.yml +++ b/.github/workflows/critical-vuln-check.yml @@ -1,9 +1,9 @@ # ============================================================================= -# Critical Dependabot Vulnerability Check — Caller template +# Critical Dependabot Vulnerability Check - Caller template # ============================================================================= # USAGE: # Copy this file to your repo at .github/workflows/critical-vuln-check.yml. -# No REPLACE values needed — this template requires no customization. +# No REPLACE values needed - this template requires no customization. # # WHAT IT DOES: # Runs on every PR targeting main or develop. Fails if the repository has @@ -11,9 +11,9 @@ # # Enforcement differs by target branch via each repo's OWN branch # protection settings (not by anything in this file): -# - On `main`: mark this check REQUIRED in branch protection — merge +# - On `main`: mark this check REQUIRED in branch protection - merge # is physically blocked while a critical alert is open. -# - On `develop`: leave this check NOT required — it still shows as a +# - On `develop`: leave this check NOT required - it still shows as a # failing/red check (a visible warning) without blocking # the merge. # ============================================================================= diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index b5fa4546..48ce1b7b 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,5 +1,5 @@ # ============================================================================= -# Dependabot Auto-Merge — Caller template +# Dependabot Auto-Merge - Caller template # ============================================================================= # USAGE: # Copy this file to your repo at .github/workflows/dependabot-auto-merge.yml. @@ -10,13 +10,13 @@ # - The PR author is dependabot[bot] # - The update is a semver PATCH bump (never minor/major) # - The ecosystem is npm, nuget, pub, bundler, or github-actions -# (NEVER docker — base image bumps always need manual review) +# (NEVER docker - base image bumps always need manual review) # - This repo currently has no open critical Dependabot alert (re-checked -# here explicitly — see workflow-templates/critical-vuln-check.yml's +# here explicitly - see workflow-templates/critical-vuln-check.yml's # header for why this can't just `needs:` that file's job) # "Auto-merge" here means GitHub's native auto-merge feature: it still # waits for the repo's actual required status checks (build/test) to pass -# before merging — this workflow does not bypass those. +# before merging - this workflow does not bypass those. # ============================================================================= name: Dependabot Auto-Merge @@ -64,5 +64,5 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} run: | set -euo pipefail - echo "::notice title=🤖 [AUTO-MERGE] Enabling auto-merge::${PR_URL} — patch-level ${{ steps.metadata.outputs.package-ecosystem }} bump, vuln gate passed" + echo "::notice title=🤖 [AUTO-MERGE] Enabling auto-merge::${PR_URL} - patch-level ${{ steps.metadata.outputs.package-ecosystem }} bump, vuln gate passed" gh pr merge --auto --squash "$PR_URL" diff --git a/.github/workflows/dotnet-pr-checks.yml b/.github/workflows/dotnet-pr-checks.yml new file mode 100644 index 00000000..06025989 --- /dev/null +++ b/.github/workflows/dotnet-pr-checks.yml @@ -0,0 +1,50 @@ +# Builds the solution and runs the .NET unit tests on every pull request into a release branch. +# +# The deploy pipeline runs the .NET tests only after a merge, so a change that broke them was +# found once it had already landed on a release branch. Bitween runs at customer sites that +# upgrade in place, so this check runs before the merge instead. The unit tests need no +# database or broker and take seconds. The integration tests (Testcontainers) still run in the +# deploy pipeline. +name: .NET checks + +on: + pull_request: + branches: [releases/**] + paths-ignore: + # Covered by frontend-tests.yml; the .NET build does not compile the SPA. + - 'SW.Bitween.Web/ClientApp/**' + - 'docs/**' + - '**/*.md' + +permissions: + contents: read + +concurrency: + group: dotnet-pr-checks-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build-and-unit-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + # Pinned to commits, since a tag can be moved. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '10.0.x' + - run: dotnet restore SW.Bitween.sln + # SkipClientBuild keeps MSBuild from building the SPA should any target pull in publish. + - run: dotnet build SW.Bitween.sln --no-restore --configuration Release -p:SkipClientBuild=true + - run: > + dotnet test SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj + --no-build --configuration Release + --logger "trx;LogFileName=unit-tests.trx" + --results-directory TestResults + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: failure() + with: + name: unit-test-results + path: TestResults diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 0091a1b3..fabe5644 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -3,7 +3,7 @@ # # The deploy pipeline only runs the .NET test projects, and only after a merge, so until # this existed nothing ever ran the frontend tests outside someone's machine. These need -# no backend or database — the component tests render pages against a mock network — so +# no backend or database - the component tests render pages against a mock network - so # they take seconds. The Playwright e2e suite is not run here: it needs a running backend # with seeded data. name: Frontend tests @@ -40,7 +40,7 @@ jobs: cache: yarn cache-dependency-path: SW.Bitween.Web/ClientApp/yarn.lock - run: yarn install --frozen-lockfile - # The same type-check the Docker build runs, which covers the tests too — so a test file + # The same type-check the Docker build runs, which covers the tests too - so a test file # that breaks it fails here rather than in the deploy after the merge. - run: npx tsc -b - run: yarn test diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml new file mode 100644 index 00000000..58af9e68 --- /dev/null +++ b/.github/workflows/sdk-release.yml @@ -0,0 +1,95 @@ +name: SDK release + +# Publishes the adapter contract for Python to PyPI as simplyworks-bitween and for JavaScript and +# TypeScript to npm as @simplyworks/bitween. Never run by hand or by a tag: the release run on +# releases/r10.0 (bitween-api-cicd-gateway.yml) calls it with the version it computed, so they carry +# Bitween's version. Only a release version is published. Each is tested first against +# SW-Serverless's SDK from its default branch, and a version already on its registry is left alone, +# so a failed run can simply be run again. +on: + workflow_call: + inputs: + version: + description: "The version the calling run computed" + type: string + required: true + +permissions: + contents: read + +jobs: + python: + if: ${{ !contains(inputs.version, '-') }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/python + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: simplify9/SW-Serverless + path: sw-serverless + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Version + id: version + env: + VERSION: ${{ inputs.version }} + run: | + v="$VERSION" + sed -i -E "s/^version = \".*\"/version = \"$v\"/" pyproject.toml + sed -i -E "s/^__version__ = \".*\"/__version__ = \"$v\"/" src/simplyworks_bitween/__init__.py + echo "value=$v" >> "$GITHUB_OUTPUT" + - name: Test + run: PYTHONPATH="src:$GITHUB_WORKSPACE/sw-serverless/sdk/python/src" python -m unittest discover -s tests + - name: Build + run: | + python -m pip install --upgrade build + python -m build + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: sdk/python/dist + password: ${{ secrets.PYPI_API_TOKEN }} + skip-existing: true + + node: + if: ${{ !contains(inputs.version, '-') }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/node + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: simplify9/SW-Serverless + path: sw-serverless + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - name: Version + id: version + env: + VERSION: ${{ inputs.version }} + run: | + v="$VERSION" + npm version "$v" --no-git-tag-version --allow-same-version + sed -i -E "s/^const VERSION = \".*\";/const VERSION = \"$v\";/" src/index.js + echo "value=$v" >> "$GITHUB_OUTPUT" + - name: Test + env: + SW_SERVERLESS_NODE: ${{ github.workspace }}/sw-serverless/sdk/node/src + run: node --test + - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + name=$(node -p "require('./package.json').name") + if npm view "$name@${{ steps.version.outputs.value }}" version >/dev/null 2>&1; then + echo "$name@${{ steps.version.outputs.value }} is already on npm." + else + npm publish --access public + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1b7e245..04e60aac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -269,7 +269,7 @@ dotnet test --collect:"XPlat Code Coverage" # Frontend, from SW.Bitween.Web/ClientApp yarn test # unit + component tests, no backend needed -yarn test:e2e # end-to-end, needs the backend running on the Local profile +yarn test:e2e # end-to-end, against a running backend; tools/e2e.sh starts one from scratch ``` ### Where a New Test Goes @@ -280,7 +280,7 @@ Four layers, fastest first. Put a test in the lowest layer that can see the bug |---|---|---| | Unit | `SW.Bitween.UnitTests` (MSTest); `ClientApp/src/**/__tests__/*.test.ts` (vitest) | Logic with no database, bus or screen: the mapping engine, transforms, schedules, parsing, the editor's reducer. | | Integration | `SW.Bitween.IntegrationTests` (xUnit, real Postgres and RabbitMQ in Docker) | Anything that crosses into the database, the bus, or a permission guard. | -| Component | `ClientApp/src/**/__tests__/*.test.tsx` (vitest in jsdom, against a mock network — see `src/__tests__/support`) | What one page shows and does: which controls appear, what a click sends, how an error reads. | +| Component | `ClientApp/src/**/__tests__/*.test.tsx` (vitest in jsdom, against a mock network - see `src/__tests__/support`) | What one page shows and does: which controls appear, what a click sends, how an error reads. | | End-to-end | `ClientApp/e2e` (Playwright, against the running backend) | A whole user journey through the real app, and what only a real browser can check: layout widths, drag and drop, two tabs. | An end-to-end test that asserts what the engine returns, or what one page renders, belongs a layer down: it runs far slower, and fails for reasons that have nothing to do with what it tests. @@ -452,7 +452,11 @@ Instances of abusive, harassing, or otherwise unacceptable behavior may be repor ## 📄 License -By contributing to Bitween, you agree that your contributions will be licensed under the MIT License. +By submitting a contribution to Bitween, you agree that: + +- your contribution is licensed under the GNU Affero General Public License v3.0, or under the MIT License if it is in `SW.Bitween.Sdk` or a `SW.Bitween.Sample*` project; +- you grant Simplify9 a perpetual, worldwide, royalty-free right to also license your contribution under other terms, including commercial licenses; and +- you have the right to make the contribution (for example, your employer permits it). --- diff --git a/Dockerfile b/Dockerfile index 676c18a7..c1bdd943 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,21 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base COPY --from=mcr.microsoft.com/dotnet/aspnet:6.0 /usr/share/dotnet/shared /usr/share/dotnet/shared +# Python, for adapters written in it: SW.Serverless starts them with python3. Ubuntu 24.04's 3.12, +# the oldest the Python SDK supports, patched with every rebuild of the image. Published adapters +# bring their own packages, vendored; pip is here for the adapter editor, which fetches a draft's +# requirements when Bitween:AdapterEditorDependencies is on. +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Node, for adapters written in JavaScript or TypeScript, from the official Node 22 LTS image. +# Published adapters bring their dependencies in node_modules; npm is here for the adapter editor, +# as pip is. +COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node +COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm +RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + WORKDIR /app EXPOSE 8080 EXPOSE 443 @@ -36,4 +51,10 @@ FROM base AS final WORKDIR /app COPY --from=publish /app/publish . COPY --from=ui-build /src/SW.Bitween.Web/wwwroot ./wwwroot +# Run as the image's built-in non-root user. The one place Bitween writes under /app is the cache +# of downloaded adapter packages (SW.Serverless AdapterLocalPath, ./adapters by default), so that +# directory alone is handed over. It is a cache: packages are fetched again from storage if it is +# empty, so a fresh container needs no volume for it. +RUN mkdir -p /app/adapters && chown $APP_UID /app/adapters +USER $APP_UID ENTRYPOINT ["dotnet", "SW.Bitween.Web.dll"] diff --git a/LICENSE b/LICENSE index 50f04ce7..be3f7b28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -MIT License - -Copyright (c) 2020 Simplify9 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..d11411dd --- /dev/null +++ b/NOTICE @@ -0,0 +1,67 @@ +Bitween +Copyright (C) 2020-2026 Simplify9 + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, version 3 (see LICENSE), together with the additional +terms below, which are made under Section 7 of that license. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + +ADDITIONAL TERMS (Section 7) +---------------------------- + +1. Attribution (Section 7(b)). Any copy or modified version of this program, + and any work based on it, must preserve the following notice in its source + code, in its documentation, and in the Appropriate Legal Notices it displays + (for example, an "About" screen): + + Based on Bitween by Simplify9 - https://github.com/simplify9/Bitween-api + +2. Modified versions (Section 7(c)). A modified version must be marked in + reasonable ways as different from the original version and must not be + presented as the official Bitween release. + +3. Trademarks (Section 7(e)). This license grants no rights to use the name + "Bitween", the Simplify9 name, or their logos, except as needed to give the + attribution notice above. A modified version distributed or offered to + others must use a different name. + + +ADDITIONAL PERMISSION (Section 7) +--------------------------------- + +Running this program, modified or not, solely for the internal operations of +your organization does not trigger Section 13 merely because your own +employees or contractors acting on your behalf interact with it over a +network. Section 13 still applies when anyone else interacts with it remotely. + + +COMPONENTS UNDER THE MIT LICENSE +-------------------------------- + +So that custom adapters built on them are not affected by the AGPL, the +following directories remain under the MIT License (see the LICENSE file in +each directory): + + SW.Bitween.Sdk (NuGet: SimplyWorks.Bitween.Sdk) + SW.Bitween.Sample* (sample adapters, handlers and mappers) + + +EARLIER VERSIONS +---------------- + +Versions of Bitween released before this notice was added were licensed under +the MIT License, and those versions remain available under it. + + +COMMERCIAL LICENSE +------------------ + +Simplify9 offers commercial licenses for uses the AGPL does not permit without +releasing source code, such as embedding Bitween in a closed-source product or +offering it as a hosted service to third parties. Contact Simplify9 at +info@simplify9.com for details. diff --git a/README.md b/README.md index af83f3a9..5dae474c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ One .NET 10 service hosts everything: the REST API, the partner-facing gateway e - **Many ways in.** Partners call an API gateway. Other systems publish to Bitween's bus, or to their own RabbitMQ or Amazon SQS broker. Scheduled jobs pull from HTTP APIs, S3, Azure Blob, SFTP/FTP, POP3 mailboxes and databases. Aggregations roll finished exchanges up on a schedule. - **One pipeline.** Every message becomes an *exchange* that runs through the same stages: filter, map, deliver, route the response. -- **Adapters for each stage.** Built-in adapters cover HTTP, S3, Azure Blob, SFTP/FTP, POP3 and SMTP, plus a visual rules-based mapper for JSON and XML. Anything else can be a custom adapter that runs out of process. +- **Adapters for each stage.** Built-in adapters cover HTTP, S3, Azure Blob, SFTP/FTP, POP3 and SMTP, plus a visual rules-based mapper for JSON and XML. Anything else can be a custom adapter, written in .NET, Python, JavaScript or TypeScript, that runs out of process. The `bitween` CLI writes, checks and publishes them, and Python, JavaScript and TypeScript adapters can also be written in the admin UI. - **Long-lived connections.** Data sources hold connections to RabbitMQ and Amazon SQS brokers and to PostgreSQL, MySQL, SQL Server and Oracle databases, so deliveries can publish messages or run SQL. - **Reliability built in.** Retry policies match failures and retry them with a delay and a shared budget. Every exchange's retries form a chain you can follow. Exhausted budgets raise alerts, and notifiers report results. - **Operable.** Queue health straight from RabbitMQ, data source health, schedule health, run history, receive attempts, a dashboard and an audit trail of every configuration change. @@ -54,6 +54,7 @@ flowchart LR | Database | PostgreSQL, SQL Server or MySQL 8 | | Message broker | RabbitMQ, with the management plugin for queue health | | Object storage | S3-compatible, Azure Blob Storage, Oracle Cloud Object Storage, or local disk in Development | +| Custom adapters in Python or JavaScript | `python3` 3.12 or later and `node` 22 or later on a Linux or macOS host. The Docker image includes both. | ## Quick start @@ -87,6 +88,7 @@ This runs Bitween locally against PostgreSQL, RabbitMQ and local-disk storage. export Token__Key="replace-with-a-random-string-of-at-least-32-chars" export Token__Issuer=bitween-local export Token__Audience=bitween-local + export Bitween__InitialAdminPassword="replace-with-your-own-password" export Bitween__RabbitMqManagementUrl=http://localhost:15672 export Bitween__RabbitMqManagementUsername=guest export Bitween__RabbitMqManagementPassword=guest @@ -94,14 +96,9 @@ This runs Bitween locally against PostgreSQL, RabbitMQ and local-disk storage. dotnet run --project SW.Bitween.Web ``` -4. Open https://localhost:5000 and sign in as the seeded administrator. +4. Open https://localhost:5000 and sign in as `admin@Bitween.systems` with the password you set in `Bitween__InitialAdminPassword`. - ``` - admin@Bitween.systems - Mtm@dmin!2 - ``` - -The database schema is created on first start. Change the administrator password straight away, and work through the [production checklist](docs/security.md#production-checklist) before exposing an instance. To use brokers or databases as data sources, see [Data sources](docs/data-sources.md#turning-them-on). +The database schema is created on first start. A new database refuses to start without `Bitween__InitialAdminPassword` (see [Configuration](docs/configuration.md#bitweeninitialadminpassword-on-a-new-installation)). Work through the [production checklist](docs/security.md#production-checklist) before exposing an instance. To use brokers or databases as data sources, see [Data sources](docs/data-sources.md#turning-them-on). ## Documentation @@ -111,7 +108,9 @@ The database schema is created on first start. Change the administrator password | [Architecture](docs/architecture.md) | Components, projects, adapter runtimes, data, messaging, storage and caching | | [Exchange pipeline](docs/exchange-pipeline.md) | What happens to a message from arrival to result | | [Entry points](docs/entry-points.md) | API gateways, bus gateways, scheduled jobs, aggregations and legacy entry points | -| [Adapters](docs/adapters.md) | The adapter contract, every built-in adapter and its properties, custom adapters | +| [Adapters](docs/adapters.md) | Every built-in adapter and its properties, custom adapters in .NET, Python and JavaScript, publishing, the adapter editor and the source viewer | +| [Adapter contract](docs/adapter-contract.md) | What Bitween calls on an adapter of each kind and what it passes, in every language | +| [The bitween CLI](docs/cli.md) | Signing in from a terminal, and writing, building, checking and publishing adapters | | [Mapping](docs/mapping.md) | The rules-based mapper and the legacy Scriban JSON mapper | | [Data sources](docs/data-sources.md) | Long-lived connections: enabling them, placement, health and resource limits | | [External brokers](docs/external-brokers.md) | Reading from and publishing to a customer's RabbitMQ or Amazon SQS | @@ -143,13 +142,26 @@ Deeper reference written alongside the features: | `SW.Bitween.Adapters.Bus.RabbitMq`, `SW.Bitween.Adapters.Bus.Sqs` | Resident broker adapters | | `SW.Bitween.Adapters.Db.*` | Resident database adapters and their shared core | | `SW.Bitween.Sdk` | Shared models and the retry policy evaluator, published to NuGet as `SimplyWorks.Bitween.Sdk` | +| `SW.Bitween.Adapters` | The adapter contract and its .NET interfaces, published to NuGet as `SimplyWorks.Bitween.Adapters` | +| `SW.Bitween.Adapters.Tooling` | Bitween's adapters on SW-Serverless's tooling: the contract, templates and Python and Node packages the CLI and the adapter editor use | +| `SW.Bitween.Cli` | The `bitween` command | +| `sdk/python`, `sdk/node` | The adapter contract for Python (`simplyworks-bitween`) and for JavaScript and TypeScript (`@simplyworks/bitween`) | | `SW.Bitween.PgSql`, `SW.Bitween.MySql`, `SW.Bitween.MsSql` | Provider-specific database contexts and migrations | | `SW.Bitween.Sample*` | Sample custom adapters, including a resident handler | -| `SW.Bitween.UnitTests`, `SW.Bitween.IntegrationTests` | Test suites | -| `tools` | Development databases for trying the database adapters | +| `SW.Bitween.UnitTests`, `SW.Bitween.IntegrationTests`, `SW.Bitween.HttpTests`, `SW.Bitween.TransportTests` | Test suites | +| `tools` | Development databases for trying the database adapters, and the end-to-end test script | +| `scripts/install-cli.sh` | Installs the `bitween` CLI from a release | | `charts/default` | Helm chart | | `Dockerfile` | Container image build | ## License -MIT. See [LICENSE](LICENSE). +Bitween is licensed under the [GNU Affero General Public License v3.0](LICENSE), with the additional terms in [NOTICE](NOTICE): + +- You can use, run and modify Bitween for free, including in production inside your organization. +- If you distribute Bitween or offer it as a service to others, you must release your source code under the AGPL and keep the "Based on Bitween by Simplify9" attribution. +- Forks and modified versions must use a different name. "Bitween" is a Simplify9 trademark. +- `SW.Bitween.Sdk` (the `SimplyWorks.Bitween.Sdk` NuGet package) and the `SW.Bitween.Sample*` projects stay under the MIT License, so your custom adapters can use any license. +- Versions released before the license change remain available under the MIT License. + +A commercial license is available from Simplify9 ([info@simplify9.com](mailto:info@simplify9.com)) for embedding Bitween in closed-source products or hosting it for third parties without releasing source code. diff --git a/SW.Bitween.Adapters.Bus.RabbitMq/RabbitBusHandler.cs b/SW.Bitween.Adapters.Bus.RabbitMq/RabbitBusHandler.cs index dfcdfe80..300ba509 100644 --- a/SW.Bitween.Adapters.Bus.RabbitMq/RabbitBusHandler.cs +++ b/SW.Bitween.Adapters.Bus.RabbitMq/RabbitBusHandler.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.Adapters.Bus.RabbitMq; /// -/// Bitween's external RabbitMQ bus provider — a broker that is NOT the internal one, owned by +/// Bitween's external RabbitMQ bus provider - a broker that is NOT the internal one, owned by /// someone else, whose queues Bitween consumes and publishes to. /// /// The ack ordering is the contract: @@ -24,7 +24,7 @@ namespace SW.Bitween.Adapters.Bus.RabbitMq; /// -> ONLY THEN BasicAck /// /// A host rejection becomes BasicNack(requeue: true), so a Bitween outage does not lose the -/// customer's messages — it just stops draining their queue, which is the correct failure. +/// customer's messages - it just stops draining their queue, which is the correct failure. /// // Two roles, one package. "bus" is what makes it configurable as a broker connection; "handler" // is what puts it in the delivery picker, because the same resident instance both consumes a @@ -39,27 +39,32 @@ public class RabbitBusHandler(IOptions options, ILogger _endpoints = new(); private readonly Dictionary _consumerTags = new(); - private long _received, _acked, _nacked, _failed, _published; + private long _received, _acked, _nacked, _failed, _published, _poisoned; + + // Failed attempts per message, for brokers that don't count them (only quorum queues send + // x-delivery-count). Keyed by message id, or by a hash of the body when there is none. + private readonly System.Collections.Concurrent.ConcurrentDictionary _attempts = new(); + private DateTimeOffset? _lastMessageOn; private string _lastError; private volatile string _state = "Starting"; // ---------------------------------------------------------------- lifecycle - public Task StartAsync(IAdapterContext context, CancellationToken cancellationToken) + public async Task StartAsync(IAdapterContext context, CancellationToken cancellationToken) { _context = context; _stopping = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -76,60 +81,70 @@ public Task StartAsync(IAdapterContext context, CancellationToken cancellationTo VirtualHost = _options.VirtualHost, RequestedConnectionTimeout = TimeSpan.FromSeconds(15), - // The supervisor owns restart policy — backoff, crash-loop quarantine, health + // The supervisor owns restart policy - backoff, crash-loop quarantine, health // write-back. A second, hidden recovery loop in here would fight it. - AutomaticRecoveryEnabled = false + AutomaticRecoveryEnabled = false, + + // Deliveries were handed to the thread pool one task each, so up to the prefetch ran at + // once. This keeps that, and keeps each delivery's body valid until its handler is done. + ConsumerDispatchConcurrency = Math.Max((ushort)1, _options.Prefetch) }; if (_options.UseSsl) factory.Ssl = new SslOption { Enabled = true, ServerName = _options.Host }; - _connection = factory.CreateConnection($"bitween-{context.InstanceKey}"); - _connection.ConnectionShutdown += (_, e) => + _connection = await factory.CreateConnectionAsync($"bitween-{context.InstanceKey}", cancellationToken); + _connection.ConnectionShutdownAsync += (_, e) => { _state = "Disconnected"; _lastError = $"{e.ReplyCode} {e.ReplyText}"; logger.LogWarning("Connection to {Host} closed: {Reason}", _options.Host, e.ReplyText); + return Task.CompletedTask; }; - _publishChannel = _connection.CreateModel(); - _consumeChannel = _connection.CreateModel(); - _consumeChannel.BasicQos(0, _options.Prefetch, global: false); + // With confirmations tracked, a publish completes only once the broker has confirmed it, + // and throws when the broker refuses it or - mandatory - routes it nowhere. + _publishChannel = await _connection.CreateChannelAsync( + new CreateChannelOptions( + publisherConfirmationsEnabled: _options.PublisherConfirms, + publisherConfirmationTrackingEnabled: _options.PublisherConfirms), + cancellationToken); + _consumeChannel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + await _consumeChannel.BasicQosAsync(0, _options.Prefetch, global: false, cancellationToken); - DeclareTopology(_consumeChannel); + await DeclareTopologyAsync(_consumeChannel, cancellationToken); // Manage-only: the connection is up and the topology is declared, but nothing is consumed. // TestConnection still checks every endpoint, because it reads _endpoints rather than the - // consumers — so a test proves the queues exist without draining them. + // consumers - so a test proves the queues exist without draining them. if (!_options.Consume) { _state = "Idle"; logger.LogInformation("Connected to {Host} without consuming (Consume=false).", _options.Host); - return Task.CompletedTask; + return; } foreach (var endpoint in _endpoints) { - var consumer = new EventingBasicConsumer(_consumeChannel); - consumer.Received += (_, delivery) => _ = Task.Run(() => HandleAsync(endpoint, delivery)); + var consumer = new AsyncEventingBasicConsumer(_consumeChannel); + consumer.ReceivedAsync += (_, delivery) => HandleAsync(endpoint, delivery); // autoAck: false is what makes persist-then-ack possible at all. - _consumerTags[endpoint] = _consumeChannel.BasicConsume(endpoint, autoAck: false, consumer); + _consumerTags[endpoint] = await _consumeChannel.BasicConsumeAsync(endpoint, autoAck: false, consumer, + cancellationToken); } _state = _endpoints.Count == 0 ? "Idle" : "Connected"; logger.LogInformation("Connected to {Host}:{Port}{VHost}, consuming {Count} endpoint(s) with prefetch {Prefetch}.", _options.Host, _options.Port, _options.VirtualHost, _endpoints.Count, _options.Prefetch); - - return Task.CompletedTask; } - private void DeclareTopology(IModel channel) + private async Task DeclareTopologyAsync(IChannel channel, CancellationToken cancellationToken) { var mode = (_options.DeclareMode ?? "assert").ToLowerInvariant(); if (mode == "none") return; if (mode == "create" && !string.IsNullOrWhiteSpace(_options.Exchange)) - channel.ExchangeDeclare(_options.Exchange, _options.ExchangeType ?? "topic", - durable: _options.Durable, autoDelete: false); + await channel.ExchangeDeclareAsync(_options.Exchange, _options.ExchangeType ?? "topic", + durable: _options.Durable, autoDelete: false, cancellationToken: cancellationToken); foreach (var endpoint in _endpoints) { @@ -137,44 +152,49 @@ private void DeclareTopology(IModel channel) { // Throws if it does not exist, which is what we want: better to fail at start than // to silently create a queue on someone else's broker. - channel.QueueDeclarePassive(endpoint); + await channel.QueueDeclarePassiveAsync(endpoint, cancellationToken); continue; } var arguments = new Dictionary(); if (!string.IsNullOrWhiteSpace(_options.QueueType)) arguments["x-queue-type"] = _options.QueueType; - channel.QueueDeclare(endpoint, durable: _options.Durable, exclusive: false, - autoDelete: false, arguments: arguments.Count == 0 ? null : arguments); + await channel.QueueDeclareAsync(endpoint, durable: _options.Durable, exclusive: false, + autoDelete: false, arguments: arguments.Count == 0 ? null : arguments, + cancellationToken: cancellationToken); if (!string.IsNullOrWhiteSpace(_options.Exchange)) - channel.QueueBind(endpoint, _options.Exchange, _options.RoutingKey ?? endpoint); + await channel.QueueBindAsync(endpoint, _options.Exchange, _options.RoutingKey ?? endpoint, + cancellationToken: cancellationToken); } } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { _state = "Draining"; _stopping?.Cancel(); - lock (_consumeGate) + await _consumeGate.WaitAsync(CancellationToken.None); + try { foreach (var tag in _consumerTags.Values) - try { _consumeChannel?.BasicCancel(tag); } catch { } + try { if (_consumeChannel != null) await _consumeChannel.BasicCancelAsync(tag); } catch { } - try { _consumeChannel?.Close(); } catch { } + try { if (_consumeChannel != null) await _consumeChannel.CloseAsync(); } catch { } } + finally { _consumeGate.Release(); } + + await _publishGate.WaitAsync(CancellationToken.None); + try { if (_publishChannel != null) await _publishChannel.CloseAsync(); } catch { } + finally { _publishGate.Release(); } - lock (_publishGate) - try { _publishChannel?.Close(); } catch { } - try { _connection?.Close(TimeSpan.FromSeconds(3)); } catch { } + try { if (_connection != null) await _connection.CloseAsync(TimeSpan.FromSeconds(3)); } catch { } _connection?.Dispose(); _state = "Stopped"; - return Task.CompletedTask; } - public Task GetStatusAsync() + public async Task GetStatusAsync() { var status = new AdapterStatus { @@ -195,18 +215,20 @@ public Task GetStatusAsync() status.Details["failed"] = _failed.ToString(); status.Details["published"] = _published.ToString(); + status.Details["poisoned"] = _poisoned.ToString(); + foreach (var endpoint in _endpoints) - status.Details[$"depth:{endpoint}"] = Depth(endpoint)?.ToString() ?? "?"; + status.Details[$"depth:{endpoint}"] = (await DepthAsync(endpoint))?.ToString() ?? "?"; - return Task.FromResult(status); + return status; } - private uint? Depth(string queue) + private async Task DepthAsync(string queue) { try { - using var probe = _connection.CreateModel(); - return probe.QueueDeclarePassive(queue).MessageCount; + await using var probe = await _connection.CreateChannelAsync(); + return (await probe.QueueDeclarePassiveAsync(queue)).MessageCount; } catch { return null; } } @@ -230,8 +252,8 @@ private async Task HandleAsync(string endpoint, BasicDeliverEventArgs delivery) var result = await _context.PublishAsync( delivery.Body, - // The broker's own message id, and nothing else. NOT the delivery tag — tags are - // per channel and restart at 1 on every reconnect — and NOT a content hash: two + // The broker's own message id, and nothing else. NOT the delivery tag - tags are + // per channel and restart at 1 on every reconnect - and NOT a content hash: two // messages that legitimately carry the same body are two messages, and hashing // them would silently drop the second for the whole deduplication window. An // unkeyed delivery is handled once per delivery, which is the honest answer when @@ -246,30 +268,138 @@ private async Task HandleAsync(string endpoint, BasicDeliverEventArgs delivery) if (result.Accepted) { - lock (_consumeGate) _consumeChannel.BasicAck(delivery.DeliveryTag, multiple: false); + await OnConsumeChannelAsync(c => c.BasicAckAsync(delivery.DeliveryTag, multiple: false)); + _attempts.TryRemove(AttemptKey(endpoint, delivery), out _); Interlocked.Increment(ref _acked); _lastMessageOn = DateTimeOffset.UtcNow; _context.Metric("bitween.bus.rabbitmq.acked", 1); } else { - lock (_consumeGate) _consumeChannel.BasicNack(delivery.DeliveryTag, multiple: false, requeue: true); Interlocked.Increment(ref _nacked); _lastError = result.Error; - logger.LogWarning("Bitween rejected a message from {Endpoint}: {Error}. Requeued.", - endpoint, result.Error); + await RejectAsync(endpoint, delivery, result.Error); } } catch (OperationCanceledException) { - try { lock (_consumeGate) _consumeChannel.BasicNack(delivery.DeliveryTag, false, requeue: true); } catch { } + try { await OnConsumeChannelAsync(c => c.BasicNackAsync(delivery.DeliveryTag, false, requeue: true)); } catch { } } catch (Exception ex) { Interlocked.Increment(ref _failed); _lastError = ex.Message; logger.LogError(ex, "Failed to hand a delivery from {Endpoint} to Bitween.", endpoint); - try { lock (_consumeGate) _consumeChannel.BasicNack(delivery.DeliveryTag, false, requeue: true); } catch { } + try { await RejectAsync(endpoint, delivery, ex.Message); } catch { } + } + } + + /// + /// A message Bitween did not take. It used to go straight back on the queue, so one that always + /// fails came straight back too - round and round, as fast as the broker could deliver it, + /// holding a prefetch slot and filling the log. Now each requeue waits a little longer, and + /// after the poison action takes over. + /// + private async Task RejectAsync(string endpoint, BasicDeliverEventArgs delivery, string error) + { + var attempt = AttemptOf(endpoint, delivery); + var action = (_options.PoisonAction ?? "requeue").ToLowerInvariant(); + + if (_options.MaxDeliveryAttempts > 0 && attempt >= _options.MaxDeliveryAttempts && action != "requeue") + { + if (action == "park" && !string.IsNullOrWhiteSpace(_options.ParkingQueue)) + { + await ParkAsync(endpoint, delivery, error, attempt); + await OnConsumeChannelAsync(c => c.BasicAckAsync(delivery.DeliveryTag, multiple: false)); + } + else + { + await OnConsumeChannelAsync(c => c.BasicNackAsync(delivery.DeliveryTag, multiple: false, requeue: false)); + } + + _attempts.TryRemove(AttemptKey(endpoint, delivery), out _); + Interlocked.Increment(ref _poisoned); + logger.LogError("A message from {Endpoint} failed {Attempts} times and was {Action}: {Error}", + endpoint, attempt, action == "park" ? $"parked on {_options.ParkingQueue}" : "rejected", error); + return; + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(Math.Min(attempt, 10)), _stopping.Token); + } + catch (OperationCanceledException) { /* stopping: requeue straight away */ } + + await OnConsumeChannelAsync(c => c.BasicNackAsync(delivery.DeliveryTag, multiple: false, requeue: true)); + logger.LogWarning("Bitween rejected a message from {Endpoint} (attempt {Attempt}): {Error}. Requeued.", + endpoint, attempt, error); + } + + private int AttemptOf(string endpoint, BasicDeliverEventArgs delivery) + { + // Quorum queues count deliveries themselves, across restarts and nodes. + if (delivery.BasicProperties?.Headers?.TryGetValue("x-delivery-count", out var counted) == true && + counted is not null && long.TryParse(counted.ToString(), out var deliveries)) + return (int)deliveries + 1; + + if (_attempts.Count > 100_000) _attempts.Clear(); + return _attempts.AddOrUpdate(AttemptKey(endpoint, delivery), 1, (_, n) => n + 1); + } + + private static string AttemptKey(string endpoint, BasicDeliverEventArgs delivery) => + endpoint + ":" + (delivery.BasicProperties?.MessageId is { Length: > 0 } id + ? id + : Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(delivery.Body.Span))); + + private async Task OnConsumeChannelAsync(Func action) + { + await _consumeGate.WaitAsync(); + try { await action(_consumeChannel); } + finally { _consumeGate.Release(); } + } + + private async Task ParkAsync(string endpoint, BasicDeliverEventArgs delivery, string error, int attempts) + { + var headers = new Dictionary(); + if (delivery.BasicProperties?.Headers is { } original) + foreach (var (key, value) in original) headers[key] = value; + headers["x-bitween-error"] = error ?? ""; + headers["x-bitween-endpoint"] = endpoint; + headers["x-bitween-attempts"] = attempts; + + var properties = new BasicProperties + { + ContentType = delivery.BasicProperties?.ContentType, + MessageId = delivery.BasicProperties?.MessageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = headers + }; + + await PublishConfirmedAsync("", _options.ParkingQueue, mandatory: false, properties, delivery.Body); + } + + /// + /// Publishes and, with confirmations on, returns only once the broker has confirmed it. Without + /// that, a message the broker dropped - a full queue, a failover - was still recorded as + /// delivered. A refusal, or a mandatory message routed nowhere, throws. + /// + private async Task PublishConfirmedAsync(string exchange, string routingKey, bool mandatory, + BasicProperties properties, ReadOnlyMemory body) + { + await _publishGate.WaitAsync(); + try + { + await _publishChannel.BasicPublishAsync(exchange, routingKey, mandatory, properties, body); + } + catch (RabbitMQ.Client.Exceptions.PublishException ex) + { + throw new InvalidOperationException(ex.IsReturn + ? "No queue received the message; the broker returned it as unroutable." + : "The broker refused the message.", ex); + } + finally + { + _publishGate.Release(); } } @@ -291,7 +421,7 @@ private string WarnUnkeyed(string endpoint) /// Egress. Bitween does not have this on the internal gateway yet; an external provider gets /// it for free because the adapter owns the connection either way. /// - public Task Publish(PublishRequest request) + public async Task Publish(PublishRequest request) { if (string.IsNullOrWhiteSpace(request?.Endpoint) && string.IsNullOrWhiteSpace(request?.Exchange)) throw new ArgumentException("Either Endpoint or Exchange is required."); @@ -299,39 +429,36 @@ public Task Publish(PublishRequest request) var body = System.Text.Encoding.UTF8.GetBytes(request.Body ?? ""); var messageId = request.MessageId ?? Guid.NewGuid().ToString("N"); - // CreateBasicProperties is itself a channel operation, so it belongs inside the gate with - // the publish rather than beside it. - lock (_publishGate) + var properties = new BasicProperties { - var properties = _publishChannel.CreateBasicProperties(); - properties.ContentType = request.ContentType ?? "application/json"; - properties.MessageId = messageId; - properties.DeliveryMode = (byte)(_options.Durable ? 2 : 1); - - _publishChannel.BasicPublish( - exchange: request.Exchange ?? "", - routingKey: request.Exchange == null ? request.Endpoint : request.RoutingKey ?? "", - mandatory: false, - basicProperties: properties, - body: body); - } + ContentType = request.ContentType ?? "application/json", + MessageId = messageId, + DeliveryMode = _options.Durable ? DeliveryModes.Persistent : DeliveryModes.Transient + }; + + await PublishConfirmedAsync( + exchange: request.Exchange ?? "", + routingKey: request.Exchange == null ? request.Endpoint : request.RoutingKey ?? "", + mandatory: _options.Mandatory, + properties, + body); Interlocked.Increment(ref _published); - return Task.FromResult(new { messageId, bytes = body.Length }); + return new { messageId, bytes = body.Length }; } /// /// Egress through the PIPELINE: a subscription's delivery stage, publishing the message it was /// given to a queue on this broker. /// - /// has existed since this adapter did, and nothing could reach it — + /// has existed since this adapter did, and nothing could reach it - /// Bitween's pipeline calls Handle on a handler, and this class had none, so an operator /// could consume from a customer's broker and had no way to answer on it. This is the two of /// them joined up; the publishing itself is unchanged. /// /// Where to send is the SUBSCRIPTION's business, not the connection's: one instance serves /// every gateway on this broker, so the endpoint travels with the call rather than with the - /// process. Deliberately NOT restricted to the endpoints the data source consumes — the common + /// process. Deliberately NOT restricted to the endpoints the data source consumes - the common /// case for egress is a queue Bitween does not drain. /// public Task Handle(XchangeFile xchangeFile) @@ -372,7 +499,7 @@ async Task PublishAsFile(XchangeFile xchangeFile, string endpoint, } /// The control the UI needs before a data source is saved. Staged, so a failure names the step. - public Task TestConnection() + public async Task TestConnection() { var steps = new List(); IConnection probe = null; @@ -387,52 +514,60 @@ public Task TestConnection() }; if (_options.UseSsl) factory.Ssl = new SslOption { Enabled = true, ServerName = _options.Host }; - probe = factory.CreateConnection("bitween-probe"); + probe = await factory.CreateConnectionAsync("bitween-probe"); steps.Add(new { step = "connect", ok = true, detail = probe.Endpoint.ToString() }); - using var channel = probe.CreateModel(); + await using var channel = await probe.CreateChannelAsync(); steps.Add(new { step = "authenticate", ok = true, detail = _options.VirtualHost }); foreach (var endpoint in _endpoints) { try { - var declared = channel.QueueDeclarePassive(endpoint); + var declared = await channel.QueueDeclarePassiveAsync(endpoint); steps.Add(new { step = $"queue:{endpoint}", ok = true, detail = $"{declared.MessageCount} message(s)" }); } catch (Exception ex) { steps.Add(new { step = $"queue:{endpoint}", ok = false, detail = ex.Message }); - return Task.FromResult(new { ok = false, steps }); + return new { ok = false, steps }; } } - return Task.FromResult(new { ok = true, steps }); + return new { ok = true, steps }; } catch (Exception ex) { steps.Add(new { step = "failed", ok = false, detail = ex.Message }); - return Task.FromResult(new { ok = false, steps }); + return new { ok = false, steps }; } finally { - try { probe?.Close(); probe?.Dispose(); } catch { } + try { if (probe != null) await probe.CloseAsync(); probe?.Dispose(); } catch { } } } /// What is actually on the broker, for the "pick a queue" step in the UI. - public Task Discover() => Task.FromResult(new + public async Task Discover() { - host = $"{_options.Host}:{_options.Port}", - virtualHost = _options.VirtualHost, - endpoints = _endpoints.Select(e => new { name = e, messages = Depth(e), consuming = _consumerTags.ContainsKey(e) }), - note = "AMQP alone can only report on queues we were told about. " + - "Listing everything on the broker needs the management plugin." - }); + var endpoints = new List(); + foreach (var e in _endpoints) + endpoints.Add(new { name = e, messages = await DepthAsync(e), consuming = _consumerTags.ContainsKey(e) }); + + return new + { + host = $"{_options.Host}:{_options.Port}", + virtualHost = _options.VirtualHost, + endpoints, + note = "AMQP alone can only report on queues we were told about. " + + "Listing everything on the broker needs the management plugin." + }; + } public Task GetStats() => Task.FromResult(new { received = _received, acked = _acked, nacked = _nacked, failed = _failed, published = _published, + poisoned = _poisoned, endpoints = _endpoints, prefetch = _options.Prefetch }); diff --git a/SW.Bitween.Adapters.Bus.RabbitMq/RabbitOptions.cs b/SW.Bitween.Adapters.Bus.RabbitMq/RabbitOptions.cs index d9cd3d85..7a08a904 100644 --- a/SW.Bitween.Adapters.Bus.RabbitMq/RabbitOptions.cs +++ b/SW.Bitween.Adapters.Bus.RabbitMq/RabbitOptions.cs @@ -3,7 +3,7 @@ namespace SW.Bitween.Adapters.Bus.RabbitMq; /// /// Every one of these arrives as a DataSource property, bound by name. Nothing here is opinionated /// about how the broker should be laid out: the queue may already exist and be owned by someone -/// else, or Bitween may declare it — decides which. +/// else, or Bitween may declare it - decides which. /// /// The attributes are not documentation: Bitween reads them out of this assembly to build the form /// an operator fills in, so a field added here appears there without anyone editing the UI. @@ -15,7 +15,7 @@ namespace SW.Bitween.Adapters.Bus.RabbitMq; public class RabbitOptions { [AdapterSetting(Required = true, - Hint = "Host name only — no amqp:// or amqps:// prefix, and no credentials.")] + Hint = "Host name only - no amqp:// or amqps:// prefix, and no credentials.")] public string Host { get; set; } = "localhost"; [AdapterSetting(Default = "5672", @@ -33,7 +33,7 @@ public class RabbitOptions public string VirtualHost { get; set; } = "/"; [AdapterSetting(Default = "false", AllowedValues = new[] {"true", "false"}, - Hint = "Set for anything that is not localhost — AMQP authenticates in the clear otherwise. " + Hint = "Set for anything that is not localhost - AMQP authenticates in the clear otherwise. " + "Pair it with port 5671.")] public bool UseSsl { get; set; } @@ -42,13 +42,13 @@ public class RabbitOptions public string Endpoints { get; set; } /// - /// none — assume everything exists; never touch the topology. - /// assert — verify it exists and fail loudly if not (passive declare). - /// create — declare queues, and an exchange and binding when Exchange is set. + /// none - assume everything exists; never touch the topology. + /// assert - verify it exists and fail loudly if not (passive declare). + /// create - declare queues, and an exchange and binding when Exchange is set. /// [AdapterSetting(Default = "assert", AllowedValues = new[] {"none", "assert", "create"}, - Hint = "none — assume everything exists. assert — check and fail loudly if not. " - + "create — declare the queues.")] + Hint = "none - assume everything exists. assert - check and fail loudly if not. " + + "create - declare the queues.")] public string DeclareMode { get; set; } = "assert"; /// Optional. When set, each endpoint queue is bound to it. @@ -83,4 +83,45 @@ public class RabbitOptions Hint = "Passed through as x-queue-type when Bitween declares the queue. Leave empty to let " + "the broker choose. It cannot be changed on a queue that already exists.")] public string QueueType { get; set; } + + /// + /// How many times one message may fail to reach Bitween before + /// applies. Zero keeps requeueing it for good, which is what this adapter always did. + /// + [AdapterSetting(Default = "0", + Hint = "How many failed attempts before a message is treated as poison. 0 retries it for good.")] + public int MaxDeliveryAttempts { get; set; } + + /// + /// requeue - put it back and keep trying, however often it fails. + /// reject - reject it without requeue, so the broker dead-letters it if the queue has a + /// dead-letter exchange, and drops it if it does not. + /// park - copy it to with the error in its headers, then ack it. + /// + [AdapterSetting(Default = "requeue", AllowedValues = new[] {"requeue", "reject", "park"}, + Hint = "What to do with a message after MaxDeliveryAttempts failures. reject relies on the " + + "queue's dead-letter exchange; park copies it to ParkingQueue.")] + public string PoisonAction { get; set; } = "requeue"; + + [AdapterSetting(Hint = "Queue that poison messages are copied to when PoisonAction is park. It must exist.")] + public string ParkingQueue { get; set; } + + /// + /// Wait for the broker to confirm each message Bitween publishes, so a delivery that the broker + /// did not take is a failed delivery - retried by the subscription's retry policy - rather + /// than one reported as sent. + /// + [AdapterSetting(Default = "true", AllowedValues = new[] {"true", "false"}, + Hint = "Report a publish as delivered only once the broker confirms it.")] + public bool PublisherConfirms { get; set; } = true; + + /// + /// Fail a publish that no queue receives. Off by default: until now such a message was + /// reported as delivered, and a deployment may be publishing to an exchange with no binding on + /// purpose. + /// + [AdapterSetting(Default = "false", AllowedValues = new[] {"true", "false"}, + Hint = "Fail a publish that is routed to no queue, instead of reporting it as delivered. " + + "Needs PublisherConfirms, which is how the broker's return is waited for.")] + public bool Mandatory { get; set; } } diff --git a/SW.Bitween.Adapters.Bus.RabbitMq/SW.Bitween.Adapters.Bus.RabbitMq.csproj b/SW.Bitween.Adapters.Bus.RabbitMq/SW.Bitween.Adapters.Bus.RabbitMq.csproj index d180c52a..baafa8a2 100644 --- a/SW.Bitween.Adapters.Bus.RabbitMq/SW.Bitween.Adapters.Bus.RabbitMq.csproj +++ b/SW.Bitween.Adapters.Bus.RabbitMq/SW.Bitween.Adapters.Bus.RabbitMq.csproj @@ -1,20 +1,20 @@ Exe - - net8.0 + + net10.0 disable SW.Bitween.Adapters.Bus.RabbitMq - - + + - + diff --git a/SW.Bitween.Adapters.Bus.RabbitMq/adapter.json b/SW.Bitween.Adapters.Bus.RabbitMq/adapter.json new file mode 100644 index 00000000..8f9ea4c9 --- /dev/null +++ b/SW.Bitween.Adapters.Bus.RabbitMq/adapter.json @@ -0,0 +1,7 @@ +{ + "id": "bitween.bus.rabbitmq", + "version": "1.0.0", + "displayName": "RabbitMQ", + "summary": "A customer's RabbitMQ broker as a data source: bus gateways read from its queues, subscriptions publish to it.", + "lifecycle": "resident" +} diff --git a/SW.Bitween.Adapters.Bus.Sqs/SW.Bitween.Adapters.Bus.Sqs.csproj b/SW.Bitween.Adapters.Bus.Sqs/SW.Bitween.Adapters.Bus.Sqs.csproj index 4d35c92c..b6973fa3 100644 --- a/SW.Bitween.Adapters.Bus.Sqs/SW.Bitween.Adapters.Bus.Sqs.csproj +++ b/SW.Bitween.Adapters.Bus.Sqs/SW.Bitween.Adapters.Bus.Sqs.csproj @@ -1,17 +1,17 @@ Exe - net8.0 + net10.0 disable SW.Bitween.Adapters.Bus.Sqs - + - + diff --git a/SW.Bitween.Adapters.Bus.Sqs/SqsBusHandler.cs b/SW.Bitween.Adapters.Bus.Sqs/SqsBusHandler.cs index a16a6e0f..1e2c117c 100644 --- a/SW.Bitween.Adapters.Bus.Sqs/SqsBusHandler.cs +++ b/SW.Bitween.Adapters.Bus.Sqs/SqsBusHandler.cs @@ -22,7 +22,7 @@ namespace SW.Bitween.Adapters.Bus.Sqs; /// /// SQS is a poll-based queue, not a push broker, so the shape differs from RabbitMQ in one way /// that matters: there is no ack, only DELETE. A message stays invisible for the visibility -/// timeout and reappears if it is not deleted — which is the same at-least-once contract by a +/// timeout and reappears if it is not deleted - which is the same at-least-once contract by a /// different mechanism, and maps cleanly onto persist-then-acknowledge: /// /// receive -> PublishAsync to the host -> host persists -> ONLY THEN DeleteMessage @@ -34,8 +34,8 @@ namespace SW.Bitween.Adapters.Bus.Sqs; /// WHY THIS ONE MATTERS BEYOND SQS ITSELF: the Amazon Selling Partner API delivers notifications /// by publishing to an SQS queue that you own. You create the queue, grant SP-API permission to /// send to it, then subscribe notification types to that destination. So this adapter is the -/// transport for SP-API notifications — ORDER_CHANGE, LISTINGS_ITEM_STATUS_CHANGE, REPORT_PROCESSING_FINISHED -/// and the rest — and handles the +/// transport for SP-API notifications - ORDER_CHANGE, LISTINGS_ITEM_STATUS_CHANGE, REPORT_PROCESSING_FINISHED +/// and the rest - and handles the /// envelope they arrive in. The SP-API request/response calls themselves are ordinary HTTPS and /// belong in a mapper or handler, not here. /// @@ -98,7 +98,7 @@ private static IAmazonSQS CreateClient(SqsOptions options) config.AuthenticationRegion = options.Region; } - // No keys means the ambient chain — instance profile, IRSA, environment. That is the + // No keys means the ambient chain - instance profile, IRSA, environment. That is the // correct production answer on AWS, and explicit keys are the exception. return string.IsNullOrWhiteSpace(options.AccessKeyId) ? new AmazonSQSClient(config) @@ -248,7 +248,7 @@ private async Task HandleAsync(string queueUrl, Message message, CancellationTok } else { - // Make it visible again immediately instead of waiting out the timeout, so a + // Back on the queue after a short wait rather than the full visibility timeout, so a // transient Bitween failure retries in seconds rather than minutes. await ReturnToQueueAsync(queueUrl, message, ct); Interlocked.Increment(ref _returned); @@ -267,8 +267,21 @@ private async Task HandleAsync(string queueUrl, Message message, CancellationTok } } - private Task ReturnToQueueAsync(string queueUrl, Message message, CancellationToken ct) => - _sqs.ChangeMessageVisibilityAsync(queueUrl, message.ReceiptHandle, 0, ct); + /// + /// Puts the message back after a wait that grows with each receive: five seconds a time, up to + /// five minutes. It used to be made visible again at once, so a message that always fails was + /// received again straight away, over and over, until the queue's redrive policy moved it - or + /// for good, on a queue without one. + /// + private Task ReturnToQueueAsync(string queueUrl, Message message, CancellationToken ct) + { + var receives = message.Attributes != null && + message.Attributes.TryGetValue("ApproximateReceiveCount", out var count) && + int.TryParse(count, out var parsed) + ? parsed + : 1; + return _sqs.ChangeMessageVisibilityAsync(queueUrl, message.ReceiptHandle, Math.Min(receives * 5, 300), ct); + } /// /// SP-API notifications arrive wrapped: notificationType, notificationVersion, payloadVersion, @@ -332,13 +345,13 @@ public async Task Publish(PublishRequest request) /// Egress through the PIPELINE: a subscription's delivery stage, sending the message it was /// given to a queue these credentials can reach. /// - /// has existed since this adapter did, and nothing could reach it — + /// has existed since this adapter did, and nothing could reach it - /// Bitween's pipeline calls Handle on a handler, and this class had none. This is the /// two joined up; the sending itself is unchanged. /// /// The queue URL is the SUBSCRIPTION's, not the connection's: one instance serves every /// gateway on these credentials, so it travels with the call. Deliberately not restricted to - /// the queues this data source consumes — the common case for egress is one it does not. + /// the queues this data source consumes - the common case for egress is one it does not. /// public async Task Handle(XchangeFile xchangeFile) { @@ -347,7 +360,7 @@ public async Task Handle(XchangeFile xchangeFile) if (string.IsNullOrWhiteSpace(endpoint)) throw new InvalidOperationException( "This delivery has no Endpoint, so there is nowhere to send. Set it to the queue " - + "URL — the full https://sqs..amazonaws.com//, which is " + + "URL - the full https://sqs..amazonaws.com//, which is " + "what the SDK addresses a queue by."); var receipt = await Publish(new PublishRequest diff --git a/SW.Bitween.Adapters.Bus.Sqs/SqsOptions.cs b/SW.Bitween.Adapters.Bus.Sqs/SqsOptions.cs index ccfa419f..abb9f940 100644 --- a/SW.Bitween.Adapters.Bus.Sqs/SqsOptions.cs +++ b/SW.Bitween.Adapters.Bus.Sqs/SqsOptions.cs @@ -2,7 +2,7 @@ namespace SW.Bitween.Adapters.Bus.Sqs; /// /// Bound by name from the DataSource's properties. The attributes are what Bitween's data source -/// form is built from — see AdapterSettingAttribute for why the UI no longer keeps its own copy of +/// form is built from - see AdapterSettingAttribute for why the UI no longer keeps its own copy of /// this list. /// [AdapterSettings( @@ -24,7 +24,7 @@ public class SqsOptions public string Region { get; set; } = "eu-west-1"; /// - /// Leave both blank to use the ambient credential chain — instance profile, IRSA, or the + /// Leave both blank to use the ambient credential chain - instance profile, IRSA, or the /// environment. That is the right answer on AWS; explicit keys are for everything else. /// [AdapterSetting(Secret = true, @@ -42,9 +42,9 @@ public class SqsOptions [AdapterSetting(Hidden = true)] public string Endpoints { get; set; } - /// Long polling. 20 is the maximum and the only sensible value — 0 burns money and API calls. + /// Long polling. 20 is the maximum and the only sensible value - 0 burns money and API calls. [AdapterSetting(Default = "20", - Hint = "Long polling, in seconds. 20 is the maximum and the only sensible value — 0 burns " + Hint = "Long polling, in seconds. 20 is the maximum and the only sensible value - 0 burns " + "money and API calls.")] public int WaitTimeSeconds { get; set; } = 20; diff --git a/SW.Bitween.Adapters.Bus.Sqs/adapter.json b/SW.Bitween.Adapters.Bus.Sqs/adapter.json new file mode 100644 index 00000000..a270e9f6 --- /dev/null +++ b/SW.Bitween.Adapters.Bus.Sqs/adapter.json @@ -0,0 +1,7 @@ +{ + "id": "bitween.bus.sqs", + "version": "1.0.0", + "displayName": "Amazon SQS", + "summary": "An Amazon SQS queue, or one compatible with it, as a data source: bus gateways read from it, subscriptions send to it.", + "lifecycle": "resident" +} diff --git a/SW.Bitween.Adapters.Db.Core/DbContracts.cs b/SW.Bitween.Adapters.Db.Core/DbContracts.cs index ff8f02ed..d1dc1203 100644 --- a/SW.Bitween.Adapters.Db.Core/DbContracts.cs +++ b/SW.Bitween.Adapters.Db.Core/DbContracts.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Adapters.Db; // Every contract below is serialised camelCase, and it is declared per type rather than left to the // host's serializer settings: these objects cross a process boundary as JSON, and the reader on the // far side is Bitween's UI, which reads what the bus adapters already emit. Those are anonymous -// objects, so they are camelCase by construction — a typed contract defaulting to PascalCase would +// objects, so they are camelCase by construction - a typed contract defaulting to PascalCase would // have the data source screen reading `Ok` from one provider and `ok` from the next. // ---------------------------------------------------------------------------- capabilities @@ -16,8 +16,8 @@ namespace SW.Bitween.Adapters.Db; /// /// What this engine, through this login, can actually be asked to do. /// -/// Answered offline-ish — most of it is a constant per adapter, the rest is probed once at connect -/// — and cached by the UI, so a screen can grey out what will not work instead of offering it and +/// Answered offline-ish - most of it is a constant per adapter, the rest is probed once at connect +/// - and cached by the UI, so a screen can grey out what will not work instead of offering it and /// failing at run time. /// /// The privilege list matters more than the feature list. "Oracle supports change notification" is @@ -47,7 +47,7 @@ public class DbCapabilities /// COPY / SqlBulkCopy / LOAD DATA / array binding. public bool BulkCopy { get; set; } - /// MERGE, ON CONFLICT, ON DUPLICATE KEY — whatever the engine calls an upsert. + /// MERGE, ON CONFLICT, ON DUPLICATE KEY - whatever the engine calls an upsert. public bool Merge { get; set; } /// RETURNING / OUTPUT: can a write hand back the rows it wrote. @@ -71,7 +71,7 @@ public class DbCapabilities public string[] ReceiveModes { get; set; } = Array.Empty(); /// - /// How this engine writes a bind placeholder — : or @ — filled in by the base + /// How this engine writes a bind placeholder - : or @ - filled in by the base /// from the adapter's own setting rather than declared per engine, so the two cannot disagree. /// /// Here because a caller that WRITES SQL needs it: the schema browser drafts a statement from @@ -90,7 +90,7 @@ public class DbCapabilities /// public string LimitStyle { get; set; } = "limit"; - /// Probed with the real credentials — what the engine allows AND this login has. + /// Probed with the real credentials - what the engine allows AND this login has. public List Privileges { get; set; } = new(); /// Anything else worth showing: NLS settings, edition, connection pool ceiling. @@ -105,7 +105,7 @@ public class DiscoverRequest /// table | view | procedure | function | sequence | package. Null means tables. public string ObjectType { get; set; } - /// Null means every schema this login can see — which on Oracle is a great many. + /// Null means every schema this login can see - which on Oracle is a great many. public string Schema { get; set; } /// Case-insensitive contains. Not a LIKE pattern: no wildcards to get wrong. @@ -145,7 +145,7 @@ public class DbObject /// Estimated, and null unless asked for. public long? RowCount { get; set; } - /// Empty unless IncludeColumns was set. Empty for a routine — see Parameters. + /// Empty unless IncludeColumns was set. Empty for a routine - see Parameters. public List Columns { get; set; } = new(); /// Routines only. @@ -171,7 +171,7 @@ public class DbColumn public int? Scale { get; set; } public bool PrimaryKey { get; set; } - /// Identity, sequence-defaulted, GENERATED ALWAYS — anything the database fills in. + /// Identity, sequence-defaulted, GENERATED ALWAYS - anything the database fills in. public bool Generated { get; set; } public int Ordinal { get; set; } @@ -205,7 +205,7 @@ public class StatementValidationResult public string Error { get; set; } /// - /// Set when it passed but not everything could be checked — a bare procedure name is accepted + /// Set when it passed but not everything could be checked - a bare procedure name is accepted /// without confirming the routine exists, because it is resolved when called. /// public string Note { get; set; } @@ -218,7 +218,7 @@ public class StatementValidationResult /// /// is the normal path: statements are configured on the data source and the /// message supplies parameter values only. is refused unless the data source -/// explicitly allows ad-hoc SQL, because a mapper is a template evaluated over message content — +/// explicitly allows ad-hoc SQL, because a mapper is a template evaluated over message content - /// if that can emit SQL text, every Xchange is an injection vector into the customer's database. /// [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] @@ -300,7 +300,7 @@ public class FetchRequest /// /// Staged on purpose. "It did not work" is not something an operator can act on; the failing stage -/// names what to go and fix — a host name, a password, a grant, a missing table. +/// names what to go and fix - a host name, a password, a grant, a missing table. /// [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DbTestResult diff --git a/SW.Bitween.Adapters.Db.Core/DbOptionsBase.cs b/SW.Bitween.Adapters.Db.Core/DbOptionsBase.cs index f2aa810d..4c91cad7 100644 --- a/SW.Bitween.Adapters.Db.Core/DbOptionsBase.cs +++ b/SW.Bitween.Adapters.Db.Core/DbOptionsBase.cs @@ -14,7 +14,7 @@ public abstract class DbOptionsBase /// /// The reason this adapter is resident at all. An ephemeral adapter gets a cold pool per /// process, so every Xchange pays a TCP connect, a TLS handshake and an authentication round - /// trip — 20–150 ms to a remote database, and considerably more to Oracle. A process that + /// trip - 20-150 ms to a remote database, and considerably more to Oracle. A process that /// stays up pays it once. /// [AdapterSetting(Default = "2", Hint = @@ -53,13 +53,13 @@ public abstract class DbOptionsBase /// Named statements as JSON: {"getOrder": "select * from orders where id = :id"}. /// The message supplies parameter values; it never supplies SQL. /// - /// Composed by Bitween from the data source's statement rows, not typed by an operator — which + /// Composed by Bitween from the data source's statement rows, not typed by an operator - which /// is why it is hidden. Statements are their own entity so that writing a query and changing a /// database password are different permissions; the adapter still just receives name-to-SQL and /// knows nothing about where they were kept. /// [AdapterSetting(Hidden = true, Hint = - "Named statements as JSON — {\"name\":\"select …\"}. Composed by Bitween from the data " + "Named statements as JSON - {\"name\":\"select …\"}. Composed by Bitween from the data " + "source's statements; not edited here.")] public string Statements { get; set; } @@ -79,7 +79,7 @@ public abstract class DbOptionsBase // Where these live is the point, and it is not all one place. // // A connection is shared by every subscription pointed at it, so anything that varies per - // reader cannot sit here — one data source could otherwise only ever feed one receiver. What + // reader cannot sit here - one data source could otherwise only ever feed one receiver. What // is left divides cleanly: the SQL and the shape of its rows belong to the STATEMENT, and the // reading policy belongs to the SUBSCRIPTION doing the reading. // @@ -113,7 +113,7 @@ public abstract class DbOptionsBase public string KeyColumn { get; set; } /// - /// Superseded by naming a statement as the subscription's MarkProcessedStatement — Camel's + /// Superseded by naming a statement as the subscription's MarkProcessedStatement - Camel's /// onConsume, run against each row once Bitween has accepted it. /// [AdapterSetting(Hidden = true)] diff --git a/SW.Bitween.Adapters.Db.Core/DbReceiver.cs b/SW.Bitween.Adapters.Db.Core/DbReceiver.cs index 7bb4587d..d349d697 100644 --- a/SW.Bitween.Adapters.Db.Core/DbReceiver.cs +++ b/SW.Bitween.Adapters.Db.Core/DbReceiver.cs @@ -13,8 +13,8 @@ namespace SW.Bitween.Adapters.Db; /// /// The receiver half: rows out of the database and into Bitween as Xchanges. /// -/// Modes are Kafka Connect's vocabulary, because it is the one operators already have — -/// bulk, incrementing, timestamp, timestamp+incrementing — plus +/// Modes are Kafka Connect's vocabulary, because it is the one operators already have - +/// bulk, incrementing, timestamp, timestamp+incrementing - plus /// marker, a processed-flag column, which is what enterprise integration tables actually /// tend to use. None of them can see a DELETE. That is a property of polling, not of this adapter, /// and the UI says so rather than letting someone discover it. @@ -22,7 +22,7 @@ namespace SW.Bitween.Adapters.Db; /// Two things make this safe to run at all: /// /// * The cursor is HOST-HELD, through IAdapterContext.SetStateAsync. An adapter cannot keep -/// its own progress — the supervisor restarts it and the next instance may be elsewhere — and a +/// its own progress - the supervisor restarts it and the next instance may be elsewhere - and a /// cursor that resets to the beginning replays every row already processed. /// * The cursor advances per row, in DeleteFile, which the pipeline calls only after Bitween has /// durably accepted that row. Advancing on read instead would lose rows on a crash; advancing @@ -58,13 +58,13 @@ sealed class ReceiveBatch /// /// The host holds state per (adapter, instance, name), and the instance here is the DATA - /// SOURCE — one connection shared by every subscription pointed at it. So the name has to + /// SOURCE - one connection shared by every subscription pointed at it. So the name has to /// carry the reader, or two subscriptions polling one connection share a cursor: whichever /// polls first advances it, and the rows it took are invisible to the other. No error, no /// warning, half the rows each. /// /// The subscription id arrives as a per-invocation value, which is why this is computed per - /// call rather than held in a field — the field would belong to whichever subscription + /// call rather than held in a field - the field would belong to whichever subscription /// happened to call first. /// string CursorStateName() @@ -94,15 +94,15 @@ string CursorStateName() const string CursorClaimStateName = "receive.cursor.inheritedBy"; /// - /// The saved cursor: this subscription's own, or — once, for whichever subscription asks - /// first — the unscoped one left behind by a version that did not scope them. + /// The saved cursor: this subscription's own, or - once, for whichever subscription asks + /// first - the unscoped one left behind by a version that did not scope them. /// async Task ReadCursorAsync() { var name = CursorStateName(); var saved = await Context.GetStateAsync(name, Stopping); - // Already has its own progress, or there is no subscription id to scope by — either way + // Already has its own progress, or there is no subscription id to scope by - either way // there is nothing to inherit. if (!string.IsNullOrEmpty(saved) || name == LegacyCursorStateName) return saved; @@ -122,7 +122,7 @@ async Task ReadCursorAsync() return legacy; } - // Claimed by this one already, and it has not accepted a row yet — so the inherited value + // Claimed by this one already, and it has not accepted a row yet - so the inherited value // is still where it is up to. if (claimedBy == subscriptionId) return legacy; @@ -142,7 +142,7 @@ async Task ReadCursorAsync() /// /// * The SQL is a named statement, exactly like every other statement. It is a name and /// never text, because a per-invocation property has {{partner.X}} substituted into it - /// before the adapter sees it — SQL there would be steerable by ordinary partner data. + /// before the adapter sees it - SQL there would be steerable by ordinary partner data. /// * The cursor and key columns come from that statement, because they describe what the /// query returns. The same statement returns the same cursor column whoever reads it. /// * The mode and batch size come from the subscription, because they are the reader's @@ -192,7 +192,7 @@ string Given(string name) => mine.TryGetValue(name, out var value) && !string.Is if (int.TryParse(Given("ReceiveBatchSize"), out var parsed) && parsed > 0) plan.BatchSize = parsed; - // A named statement supersedes the data source's own receive settings entirely — SQL and + // A named statement supersedes the data source's own receive settings entirely - SQL and // row shape together, because taking the SQL from one place and the cursor column from // another is how they come to disagree. var name = Given("ReceiveStatement"); @@ -233,7 +233,7 @@ string Given(string name) => mine.TryGetValue(name, out var value) && !string.Is /// /// Nothing to do. The transaction a mark-processed statement might want cannot live here: the /// pipeline calls Initialize, then ListFiles, then a GetFile and DeleteFile per row, then - /// Finalize — and holding one transaction open across all of that would pin a pooled connection + /// Finalize - and holding one transaction open across all of that would pin a pooled connection /// for as long as Bitween takes to persist every row, on a shared instance serving other /// subscriptions at the same time. Each mark-processed runs in its own transaction instead, /// after Bitween has accepted the row, which is the at-least-once boundary anyway. @@ -256,7 +256,7 @@ public virtual async Task> ListFiles() if (string.IsNullOrEmpty(plan.Mode)) throw new InvalidOperationException( "No ReceiveMode, so this cannot be used as a receiver. Set one on the subscription " - + "(or on the data source, as a default) — one of bulk, incrementing, timestamp, " + + "(or on the data source, as a default) - one of bulk, incrementing, timestamp, " + "timestamp+incrementing or marker."); if (string.IsNullOrWhiteSpace(plan.Sql)) @@ -296,7 +296,7 @@ public virtual async Task> ListFiles() MaxRows = plan.BatchSize, // Already resolved from the allow-list by Plan(), so this is configured SQL rather - // than SQL a message supplied — it does not go through the ad-hoc gate again. + // than SQL a message supplied - it does not go through the ad-hoc gate again. Name = null }, adHocAllowed: true); @@ -358,7 +358,7 @@ public virtual async Task DeleteFile(string fileId) var parameters = new Dictionary { ["key"] = RawKey(row, plan) }; // Every column is offered as a parameter too, so a mark statement can use more than the - // key — a status column, a batch id, the row's own timestamp. + // key - a status column, a batch id, the row's own timestamp. foreach (var kv in row) parameters[kv.Key] = kv.Value; await Execute(new StatementRequest @@ -379,7 +379,7 @@ await Execute(new StatementRequest /// /// Drops whatever the run did not get through. Rows left here were never accepted by Bitween - /// and the cursor never moved past them, so the next poll reads them again — which is the + /// and the cursor never moved past them, so the next poll reads them again - which is the /// correct at-least-once behaviour, not a leak. /// public virtual Task Finalize() @@ -404,7 +404,7 @@ Dictionary Locate(string fileId, out ReceiveBatch batch) if (!batches.TryGetValue(batchId, out batch)) throw new InvalidOperationException( $"Batch {batchId} is no longer held. Either the run already finished, or the adapter " - + "restarted between the listing and this call — in which case the rows were never " + + "restarted between the listing and this call - in which case the rows were never " + "acknowledged and the next poll will read them again."); if (!batch.Rows.TryGetValue(fileId, out var row)) @@ -416,7 +416,7 @@ Dictionary Locate(string fileId, out ReceiveBatch batch) static string KeyOf(string fileId) => fileId.Substring(fileId.IndexOf(':') + 1); /// - /// The row's key as the database returned it — not the string from the file id, which has been + /// The row's key as the database returned it - not the string from the file id, which has been /// through JSON and would bind a number as text. /// static object RawKey(Dictionary row, ReceivePlan plan) => @@ -424,7 +424,7 @@ static object RawKey(Dictionary row, ReceivePlan plan) => /// /// A cursor comes back from the host as text, and has to go into the query as the type the - /// column compares against — a string on one side of a timestamp comparison silently matches + /// column compares against - a string on one side of a timestamp comparison silently matches /// nothing on some engines rather than failing. /// object ParseCursor(string saved, string mode) @@ -456,7 +456,7 @@ object ParseCursor(string saved, string mode) }; /// - /// A batch whose run died mid-way — the job was cancelled, the node went down — is otherwise + /// A batch whose run died mid-way - the job was cancelled, the node went down - is otherwise /// held for the life of the process, and it holds every row of that poll in memory. /// void PruneStaleBatches() diff --git a/SW.Bitween.Adapters.Db.Core/DbResidentAdapterBase.cs b/SW.Bitween.Adapters.Db.Core/DbResidentAdapterBase.cs index 20240051..70072765 100644 --- a/SW.Bitween.Adapters.Db.Core/DbResidentAdapterBase.cs +++ b/SW.Bitween.Adapters.Db.Core/DbResidentAdapterBase.cs @@ -25,7 +25,7 @@ namespace SW.Bitween.Adapters.Db; /// handshake and an authentication round trip on every message. /// /// It is also shared. One instance serves every subscription bound to its data source, concurrently -/// — so nothing here may keep per-message state in a field. Paging cursors and receive batches are +/// - so nothing here may keep per-message state in a field. Paging cursors and receive batches are /// keyed and held in concurrent maps for exactly that reason. /// public abstract partial class DbResidentAdapterBase : IResidentAdapter, IInfolinkHandler, IInfolinkReceiver @@ -83,7 +83,7 @@ protected abstract Task> ProbePrivilegesAsync(DbConnection connecti /// The marker a parameter is written with in SQL: :, @. protected abstract string ParameterPrefix { get; } - /// Anything the driver needs before a command runs — ODP.NET's BindByName, say. + /// Anything the driver needs before a command runs - ODP.NET's BindByName, say. protected virtual void PrepareCommand(DbCommand command) { } /// A one-row, no-side-effect query used to prove the connection answers. @@ -172,7 +172,7 @@ public virtual Task GetStatusAsync() return Task.FromResult(status); } - /// Engine-specific heartbeat detail — Oracle's session count, say. + /// Engine-specific heartbeat detail - Oracle's session count, say. protected virtual IEnumerable> ExtraStatusDetails() => Array.Empty>(); @@ -192,13 +192,13 @@ protected async Task OpenAsync(CancellationToken cancellationToken /// /// The list is written per engine and is therefore about the engine at its newest. Called once /// the server version is known, which is the only point at which the difference can be told. - /// Does nothing unless an engine overrides it — most capabilities have been there for a decade. + /// Does nothing unless an engine overrides it - most capabilities have been there for a decade. /// protected virtual void AdjustForVersion(DbCapabilities described) { } /// /// The major version this server reports, or 0 when it cannot be read. Drivers spell it - /// differently — "16.14", "8.4.11", "16.00.4135" — but all of them lead with the major. + /// differently - "16.14", "8.4.11", "16.00.4135" - but all of them lead with the major. /// protected static int MajorVersionOf(string serverVersion) { @@ -213,7 +213,7 @@ protected static int MajorVersionOf(string serverVersion) /// /// For the settings an engine will not take in a connection string. PostgreSQL puts search_path /// there and Oracle takes CURRENT_SCHEMA the same way; SQL Server has neither, because its - /// default schema belongs to the login rather than to the connection — so it is the one that + /// default schema belongs to the login rather than to the connection - so it is the one that /// needs this. /// /// A pooled connection carries whatever this did into its next use, which is the point: it is @@ -236,7 +236,7 @@ DbCommand CreateCommand(DbConnection connection, string sql, IDictionary /// Values only, always as parameters. A value is never concatenated into the text, whatever it - /// looks like — that rule and the statement allow-list are the two halves of the same defence. + /// looks like - that rule and the statement allow-list are the two halves of the same defence. /// void Bind(DbCommand command, IDictionary parameters) { @@ -318,7 +318,7 @@ public virtual async Task TestConnection() // nothing. // // All of them, not up to the first failure. Stopping early meant fixing one statement - // only to be told about the next, one connection test at a time — and the second + // only to be told about the next, one connection test at a time - and the second // failure is often the same mistake repeated, which is obvious when both are on // screen and invisible when they arrive a fix apart. var bad = 0; @@ -438,7 +438,7 @@ public virtual Task GetStats() => Task.FromResult(new /// /// is for SQL that came from the DATA SOURCE rather than from a - /// message — the receive statement, the mark-processed statement. Those are configuration, so + /// message - the receive statement, the mark-processed statement. Those are configuration, so /// they are already what the allow-list is protecting; running them through it would mean /// forcing AllowAdHocSql on to use the receiver at all. /// @@ -497,7 +497,7 @@ public virtual async Task Fetch(FetchRequest request) if (!cursors.TryGetValue(request.CursorId, out var cursor)) throw new InvalidOperationException( $"Cursor '{request.CursorId}' is not open. It was either already read to the end, " - + $"closed, or left idle longer than {Options.CursorIdleTimeoutSeconds} seconds — " + + $"closed, or left idle longer than {Options.CursorIdleTimeoutSeconds} seconds - " + "an open cursor holds a pooled connection, so idle ones are reclaimed."); var take = request.Take > 0 ? request.Take : Options.MaxRows; @@ -541,7 +541,7 @@ async Task Execute(StatementRequest request, bool adHocAllowed) var result = new QueryResult(); // A write with RETURNING / OUTPUT hands back rows, and losing them would make the - // engine's most useful write form unusable — so read them when they are there. + // engine's most useful write form unusable - so read them when they are there. if (capabilities != null && capabilities.Returning) { using var reader = await command.ExecuteReaderAsync(); @@ -626,7 +626,7 @@ await ReadPageAsync(reader, result.Rows, } /// - /// Whether a statement is a bare routine name — release_order, SALES.PKG.RELEASE — + /// Whether a statement is a bare routine name - release_order, SALES.PKG.RELEASE - /// rather than SQL. Identifier characters only, so anything with a space, a parenthesis or a /// keyword is treated as SQL and prepared. /// @@ -643,19 +643,19 @@ static bool IsBareRoutineName(string sql) /// prepared without being run. Nothing is bound to a value: this is a syntax and schema check. /// /// - /// Asks the database whether one piece of SQL is valid, by PREPARING it — parsed and planned, + /// Asks the database whether one piece of SQL is valid, by PREPARING it - parsed and planned, /// never run. The check behind both the connection test and the one a statement gets when it /// is saved, so the two cannot disagree about what is acceptable. /// async Task<(bool Ok, string Detail)> CheckStatementAsync(DbConnection connection, string sql) { - // A statement meant for Call holds a PROCEDURE NAME, not SQL — that is what + // A statement meant for Call holds a PROCEDURE NAME, not SQL - that is what // CommandType.StoredProcedure takes, and on Oracle it is the only form that works. // Preparing it as text is a syntax error every time, so the check would fail on a // statement that is perfectly correct. Say what was and was not verified instead of // quietly passing it. if (IsBareRoutineName(sql)) - return (true, "procedure name — existence not checked, it is resolved when called"); + return (true, "procedure name - existence not checked, it is resolved when called"); try { @@ -673,7 +673,7 @@ static bool IsBareRoutineName(string sql) /// /// PREPARE is the portable form and is what most drivers want: the server parses the text and /// binds every name in it, which is the whole check. It is overridable because one engine - /// cannot be asked that way — see the SQL Server adapter, whose driver refuses to prepare a + /// cannot be asked that way - see the SQL Server adapter, whose driver refuses to prepare a /// command unless every parameter has been given an explicit type, which is precisely what a /// caller checking someone else's SQL does not know. /// @@ -684,7 +684,7 @@ protected virtual async Task CheckSyntaxAsync(DbConnection connection, string sq command.CommandTimeout = 10; // The placeholders have to be declared before Prepare, because some drivers validate that - // every parameter in the text has been supplied — Npgsql refuses outright — and a check + // every parameter in the text has been supplied - Npgsql refuses outright - and a check // that fell over on every parameterised statement would be worse than none. DeclarePlaceholders(command); PrepareCommand(command); @@ -695,7 +695,7 @@ protected virtual async Task CheckSyntaxAsync(DbConnection connection, string sq /// The one mistake worth naming rather than leaving to a position offset. /// /// Every engine has its own placeholder character, and the driver reports the other one as a - /// bare syntax error at a column number — true, and no help at all to someone who copied a + /// bare syntax error at a column number - true, and no help at all to someone who copied a /// working statement from an Oracle data source into a PostgreSQL one. If the SQL uses the /// other convention, say so. /// @@ -709,7 +709,7 @@ string WrongPrefixHint(string sql) var match = System.Text.RegularExpressions.Regex.Match(sql ?? "", pattern); if (!match.Success) return ""; - return $" — this looks like the wrong placeholder for {DescribeEngine().Engine}: it binds " + return $" - this looks like the wrong placeholder for {DescribeEngine().Engine}: it binds " + $"parameters as {ParameterPrefix}name, so write {ParameterPrefix}{match.Groups[1].Value} " + $"rather than {other}{match.Groups[1].Value}."; } @@ -758,7 +758,7 @@ void DeclarePlaceholders(DbCommand command) /// /// Adds one declared out parameter. Overridden where the driver needs a provider-specific type - /// — Oracle's REF CURSOR being the case that forces this to exist at all. + /// - Oracle's REF CURSOR being the case that forces this to exist at all. /// protected virtual void AddOutParameter(DbCommand command, DbRoutineParameter declared) { @@ -827,7 +827,7 @@ static IsolationLevel ParseIsolation(string level) => /// /// The subscription pipeline's handler and mapper contract. /// - /// The message body is a — or, when the subscription names a + /// The message body is a - or, when the subscription names a /// statement in its adapter properties, just the parameters. That second form is the one worth /// having: the mapper produces a flat object of values and the statement is configuration, so /// nothing about the SQL depends on message content. @@ -893,7 +893,7 @@ static Dictionary ParametersFrom(string body) "With a Statement configured, the message body has to be a JSON object of parameter " + $"names to values. This one is a {token.Type}."); - // A nested object stays as its JSON text — which is what a json column wants, and what a + // A nested object stays as its JSON text - which is what a json column wants, and what a // driver would otherwise refuse outright. return obj.Properties().ToDictionary(p => p.Name, p => (object)p.Value); } @@ -911,7 +911,7 @@ static List ColumnNames(DbDataReader reader) /// Fills one page and reports what is behind it. /// /// Returns the row that PROVES there is more. A reader cannot be rewound, so asking "is there - /// another row" consumes one — and dropping it loses exactly one row per page, which surfaces + /// another row" consumes one - and dropping it loses exactly one row per page, which surfaces /// months later as a single missing order and is close to unfindable. So it is handed back and /// carried into the next page. Null means the result set ended. /// @@ -1014,7 +1014,7 @@ void Failed() new Dictionary { ["engine"] = capabilities?.Engine ?? "" }); } - /// Recorded for the heartbeat. The message only — never the SQL, never the values. + /// Recorded for the heartbeat. The message only - never the SQL, never the values. protected void RecordError(Exception exception) { lastError = exception.Message; diff --git a/SW.Bitween.Adapters.Db.Core/SW.Bitween.Adapters.Db.Core.csproj b/SW.Bitween.Adapters.Db.Core/SW.Bitween.Adapters.Db.Core.csproj index d4e41605..1cec616b 100644 --- a/SW.Bitween.Adapters.Db.Core/SW.Bitween.Adapters.Db.Core.csproj +++ b/SW.Bitween.Adapters.Db.Core/SW.Bitween.Adapters.Db.Core.csproj @@ -1,18 +1,14 @@ - - net8.0 + + net10.0 SW.Bitween.Adapters.Db disable - + diff --git a/SW.Bitween.Adapters.Db.Core/StatementRegistry.cs b/SW.Bitween.Adapters.Db.Core/StatementRegistry.cs index 2ef89d19..0c597050 100644 --- a/SW.Bitween.Adapters.Db.Core/StatementRegistry.cs +++ b/SW.Bitween.Adapters.Db.Core/StatementRegistry.cs @@ -18,7 +18,7 @@ public class StatementRegistry { /// /// One configured statement. Most are just SQL; a statement a receiver polls with also carries - /// the shape of its rows — which column is the cursor, which identifies the row. + /// the shape of its rows - which column is the cursor, which identifies the row. /// /// Those two live with the statement rather than with the subscription reading it because they /// describe what the QUERY returns. A poll statement returns the same cursor column whoever @@ -47,7 +47,7 @@ public StatementRegistry(string json) catch (JsonException ex) { // Named rather than swallowed: an adapter that silently starts with no statements - // fails later, on a message, as "unknown statement" — which sends whoever is debugging + // fails later, on a message, as "unknown statement" - which sends whoever is debugging // it looking in the wrong place entirely. throw new ArgumentException( $"The Statements setting is not valid JSON: {ex.Message}. It should be an object " @@ -84,7 +84,7 @@ public StatementRegistry(string json) throw new ArgumentException( $"Statement '{property.Name}' has to be a string of SQL, or an object with a " - + $"'sql' property — not a {property.Value.Type}."); + + $"'sql' property - not a {property.Value.Type}."); } } @@ -112,7 +112,7 @@ public string Resolve(string name, string sql, bool allowAdHoc) throw new InvalidOperationException( $"'{name}' is not a statement this data source defines. Configured: " + (statements.Count == 0 - ? "none — set the Statements property on the data source." + ? "none - set the Statements property on the data source." : string.Join(", ", statements.Keys.OrderBy(k => k)))); } diff --git a/SW.Bitween.Adapters.Db.MySql/MySqlDbAdapter.cs b/SW.Bitween.Adapters.Db.MySql/MySqlDbAdapter.cs index c53fa7d5..5a795692 100644 --- a/SW.Bitween.Adapters.Db.MySql/MySqlDbAdapter.cs +++ b/SW.Bitween.Adapters.Db.MySql/MySqlDbAdapter.cs @@ -15,13 +15,13 @@ namespace SW.Bitween.Adapters.Db.MySql; /// /// Bitween's MySQL data source provider, which serves MariaDB too. /// -/// Everything generic — the command surface, paging, the statement allow-list, the polling receiver -/// — lives in . What is here is the part that is genuinely +/// Everything generic - the command surface, paging, the statement allow-list, the polling receiver +/// - lives in . What is here is the part that is genuinely /// MySQL: the connection string, information_schema, and what this user is actually granted. /// /// The one structural difference from the other providers is that MySQL has no schema separate /// from the database. A "schema" filter in discovery therefore means a database, and the default -/// is the one this connection opened rather than every database on the server — a service account +/// is the one this connection opened rather than every database on the server - a service account /// on a shared instance can usually see the names of databases it has no business reading. /// // Three roles, one package. "datasource" is what makes it configurable as a connection; @@ -74,7 +74,7 @@ protected override string BuildConnectionString() // A pooled connection MySQL closed at its end is indistinguishable from a healthy one // until it is used. This is what turns that into a fresh connection rather than into a - // failed message — and it matters more here than anywhere, because wait_timeout on a + // failed message - and it matters more here than anywhere, because wait_timeout on a // managed instance is routinely minutes. ConnectionReset = true, @@ -111,7 +111,7 @@ protected override string BuildConnectionString() ProcedureOutParameters = true, // The one place MySQL is more capable than PostgreSQL here: a procedure returns result sets - // simply by SELECTing, with no cursor to declare and nothing to bind — so Call returns rows + // simply by SELECTing, with no cursor to declare and nothing to bind - so Call returns rows // directly, where PostgreSQL needs a set-returning function queried with SELECT and Oracle // needs an explicit REF CURSOR. ProcedureResultSets = true, @@ -120,11 +120,11 @@ protected override string BuildConnectionString() NamedParameters = true, Transactions = true, - // All four, and READ UNCOMMITTED genuinely does something here — unlike PostgreSQL, where + // All four, and READ UNCOMMITTED genuinely does something here - unlike PostgreSQL, where // it is accepted and silently treated as READ COMMITTED. IsolationLevels = ["ReadUncommitted", "ReadCommitted", "RepeatableRead", "Serializable"], - // MySqlBulkCopy exists and is fast, but BulkLoad is not implemented for it yet — false + // MySqlBulkCopy exists and is fast, but BulkLoad is not implemented for it yet - false // rather than advertising a path that would quietly fall back to row-by-row inserts. BulkCopy = false, @@ -151,7 +151,7 @@ protected override string BuildConnectionString() /// /// What this user may do, asked of the server rather than assumed. SHOW GRANTS is the only - /// answer that accounts for roles, wildcards and grants made at every level at once — + /// answer that accounts for roles, wildcards and grants made at every level at once - /// information_schema.user_privileges reports the global ones only. /// protected override async Task> ProbePrivilegesAsync(DbConnection connection, @@ -170,7 +170,7 @@ protected override async Task> ProbePrivilegesAsync(DbConnection co { var grant = reader.GetString(0); - // "GRANT SELECT, INSERT ON `sales`.* TO ..." — the useful half is between GRANT + // "GRANT SELECT, INSERT ON `sales`.* TO ..." - the useful half is between GRANT // and ON, and the rest is the grantee, which is who we already asked about. var on = grant.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase); if (!grant.StartsWith("GRANT ", StringComparison.OrdinalIgnoreCase) || on < 0) @@ -189,7 +189,7 @@ protected override async Task> ProbePrivilegesAsync(DbConnection co } catch (Exception ex) { - // A user that cannot run SHOW GRANTS is unusual but not broken — it just cannot tell + // A user that cannot run SHOW GRANTS is unusual but not broken - it just cannot tell // us what it can do. Saying so beats failing the connection test over it. Logger.LogDebug(ex, "Could not read grants."); privileges.Add($"(could not be read: {ex.Message})"); @@ -209,7 +209,7 @@ protected override IEnumerable> ExtraStatusDetails( /// /// information_schema, which on MySQL is the catalog rather than a slow standard view over one - /// — there is no lower layer to reach for the way pg_class is under PostgreSQL's. + /// - there is no lower layer to reach for the way pg_class is under PostgreSQL's. /// protected override async Task> DiscoverAsync(DbConnection connection, DiscoverRequest request, CancellationToken cancellationToken) @@ -269,7 +269,7 @@ from information_schema.tables t // table_rows is InnoDB's estimate from the index statistics and can be out by // a wide margin on a table that has not been analysed. Reported as an estimate - // everywhere it surfaces — the alternative is COUNT(*) on a stranger's table, + // everywhere it surfaces - the alternative is COUNT(*) on a stranger's table, // which a menu should not do. Always null for a view, which has no rows of its // own to estimate. RowCount = !request.IncludeRowCounts || reader.IsDBNull(4) @@ -293,7 +293,7 @@ async Task FillColumnsAsync(DbConnection connection, List objects, // // MySqlConnector has no array parameter, so the pair of IN lists is built from the page's // own values. They are identifiers this connection just read back from the catalog, not - // anything a caller supplied — but they are quoted anyway, because "it cannot contain a + // anything a caller supplied - but they are quoted anyway, because "it cannot contain a // quote" is the kind of assumption that survives right up until a table is named oddly. var schemas = string.Join(",", objects.Select(o => Quote(o.Schema)).Distinct()); var names = string.Join(",", objects.Select(o => Quote(o.Name)).Distinct()); @@ -327,7 +327,7 @@ where c.table_schema in ({schemas}) and c.table_name in ({names}) Nullable = string.Equals(reader.GetString(4), "YES", StringComparison.OrdinalIgnoreCase), Ordinal = (int)reader.GetInt64(5), - // auto_increment, a generated column, and DEFAULT_GENERATED — which is what a + // auto_increment, a generated column, and DEFAULT_GENERATED - which is what a // CURRENT_TIMESTAMP default reports as. All three are values the database fills in, // which is the only distinction an insert cares about. Generated = extra.IndexOf("auto_increment", StringComparison.OrdinalIgnoreCase) >= 0 @@ -408,7 +408,7 @@ async Task FillParametersAsync(DbConnection connection, List routines, { // information_schema.parameters, one query for the page. The row with ordinal_position 0 // is a function's return value, which RoutinesAsync has already recorded from - // dtd_identifier — so it is skipped here rather than listed twice. + // dtd_identifier - so it is skipped here rather than listed twice. var schemas = string.Join(",", routines.Select(r => Quote(r.Schema)).Distinct()); var names = string.Join(",", routines.Select(r => Quote(r.Name)).Distinct()); @@ -453,7 +453,7 @@ and p.ordinal_position > 0 static string Quote(string value) => "'" + (value ?? "").Replace("'", "''").Replace("\\", "\\\\") + "'"; /// - /// What a value of this column arrives as in a result row. Coarse on purpose — it tells a + /// What a value of this column arrives as in a result row. Coarse on purpose - it tells a /// mapper author whether to expect a string or a number, and is not trying to be a type system. /// static string ClrTypeOf(string dbType) @@ -463,7 +463,7 @@ static string ClrTypeOf(string dbType) return bare switch { - // tinyint(1) is how MySQL stores a boolean — there is no separate type — and the + // tinyint(1) is how MySQL stores a boolean - there is no separate type - and the // driver hands it back as one, so saying "int" here would mislead a mapper author. "tinyint" when dbType.StartsWith("tinyint(1)", StringComparison.OrdinalIgnoreCase) => "bool", "bool" or "boolean" => "bool", diff --git a/SW.Bitween.Adapters.Db.MySql/MySqlOptions.cs b/SW.Bitween.Adapters.Db.MySql/MySqlOptions.cs index 3f8fdefc..44fb6518 100644 --- a/SW.Bitween.Adapters.Db.MySql/MySqlOptions.cs +++ b/SW.Bitween.Adapters.Db.MySql/MySqlOptions.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.Adapters.Db.MySql; /// /// Every setting here arrives as a DataSource property, bound by name, and the form an operator -/// fills in is generated from these attributes — so a field added here appears in Bitween with no +/// fills in is generated from these attributes - so a field added here appears in Bitween with no /// front-end change. /// /// The hints spend their weight on the two things that are genuinely MySQL's own: a database and @@ -25,13 +25,13 @@ public class MySqlOptions : DbOptionsBase public int Port { get; set; } = 3306; /// - /// MySQL has no schema separate from the database — CREATE SCHEMA is a synonym for CREATE - /// DATABASE — so this one value is both, and it is what an unqualified table name resolves + /// MySQL has no schema separate from the database - CREATE SCHEMA is a synonym for CREATE + /// DATABASE - so this one value is both, and it is what an unqualified table name resolves /// against. /// [AdapterSetting(Required = true, Hint = "The database to connect to. In MySQL a database IS a schema, so this is also what an " - + "unqualified table name resolves against — there is no separate schema setting.")] + + "unqualified table name resolves against - there is no separate schema setting.")] public string Database { get; set; } [AdapterSetting(Required = true)] @@ -54,7 +54,7 @@ public class MySqlOptions : DbOptionsBase [AdapterSetting(Default = "180", Hint = "Seconds an idle pooled connection is kept before it is closed. Keep it below the server's " - + "own wait_timeout — the default is 28800, but a managed instance or a proxy in front of " + + "own wait_timeout - the default is 28800, but a managed instance or a proxy in front of " + "one is often far lower, and a connection the server closed first surfaces as a broken " + "pipe on the next message rather than as a timeout.")] public int ConnectionIdleLifetimeSeconds { get; set; } = 180; @@ -68,7 +68,7 @@ public class MySqlOptions : DbOptionsBase [AdapterSetting(Default = "false", AllowedValues = new[] { "true", "false" }, Hint = "Return DATE and DATETIME as strings rather than dates. MySQL permits '0000-00-00', which " - + "is not a date any .NET type can hold — a column holding one throws on read unless this " + + "is not a date any .NET type can hold - a column holding one throws on read unless this " + "is on. Only needed against a schema that has them, which is usually an old one.")] public bool AllowZeroDateTime { get; set; } } diff --git a/SW.Bitween.Adapters.Db.MySql/SW.Bitween.Adapters.Db.MySql.csproj b/SW.Bitween.Adapters.Db.MySql/SW.Bitween.Adapters.Db.MySql.csproj index 10f66a09..9cafff9d 100644 --- a/SW.Bitween.Adapters.Db.MySql/SW.Bitween.Adapters.Db.MySql.csproj +++ b/SW.Bitween.Adapters.Db.MySql/SW.Bitween.Adapters.Db.MySql.csproj @@ -2,16 +2,16 @@ Exe - net8.0 + net10.0 SW.Bitween.Adapters.Db.MySql disable - + diff --git a/SW.Bitween.Adapters.Db.MySql/adapter.json b/SW.Bitween.Adapters.Db.MySql/adapter.json new file mode 100644 index 00000000..83d27c3a --- /dev/null +++ b/SW.Bitween.Adapters.Db.MySql/adapter.json @@ -0,0 +1,7 @@ +{ + "id": "bitween.db.mysql", + "version": "1.0.0", + "displayName": "MySQL", + "summary": "A MySQL database as a data source: its named statements, read from and written to.", + "lifecycle": "resident" +} diff --git a/SW.Bitween.Adapters.Db.Oracle/OracleDbAdapter.cs b/SW.Bitween.Adapters.Db.Oracle/OracleDbAdapter.cs index b50337a1..e30b21a2 100644 --- a/SW.Bitween.Adapters.Db.Oracle/OracleDbAdapter.cs +++ b/SW.Bitween.Adapters.Db.Oracle/OracleDbAdapter.cs @@ -16,8 +16,8 @@ namespace SW.Bitween.Adapters.Db.Oracle; /// /// Bitween's Oracle data source provider. /// -/// Everything generic — the command surface, paging, the statement allow-list, the polling receiver -/// — lives in . What is here is the part that is genuinely +/// Everything generic - the command surface, paging, the statement allow-list, the polling receiver +/// - lives in . What is here is the part that is genuinely /// Oracle: how a connection string is spelled, what the data dictionary is called, which privileges /// are worth probing, and REF CURSOR. /// @@ -98,14 +98,14 @@ string DataSourceOf() if (hasService && hasSid) throw new InvalidOperationException( - "Both ServiceName and Sid are set, and they are alternatives — a service name names " + "Both ServiceName and Sid are set, and they are alternatives - a service name names " + "a database service, a SID names an instance. Set whichever the DBA gave you and " + "clear the other."); if (!hasService && !hasSid) throw new InvalidOperationException( "Neither ServiceName nor Sid is set, so there is nothing to connect to on " - + $"{_options.Host}:{_options.Port}. A modern Oracle wants the SERVICE name — what " + + $"{_options.Host}:{_options.Port}. A modern Oracle wants the SERVICE name - what " + "`lsnrctl services` lists, e.g. FREEPDB1."); // Easy Connect. The slash form is the service name, the colon form is the SID. @@ -116,7 +116,7 @@ string DataSourceOf() protected override void PrepareCommand(DbCommand command) { - // Without this ODP.NET binds by POSITION, in the order parameters were added — so a + // Without this ODP.NET binds by POSITION, in the order parameters were added - so a // statement whose parameters are written in a different order than the caller supplied them // binds the wrong values, silently, with no type error to catch it. if (command is OracleCommand oracle) oracle.BindByName = _options.BindByName; @@ -126,20 +126,60 @@ protected override void PrepareCommand(DbCommand command) // ------------------------------------------------------------------ checking + private static readonly HashSet DdlKeywords = new(StringComparer.OrdinalIgnoreCase) + { + "CREATE", "ALTER", "DROP", "TRUNCATE", "GRANT", "REVOKE", "RENAME", "COMMENT", "AUDIT", + "NOAUDIT", "ANALYZE", "FLASHBACK", "PURGE", "ASSOCIATE", "DISASSOCIATE", + }; + + /// The first word of the statement, past whitespace and comments. + internal static string LeadingKeyword(string sql) + { + var i = 0; + while (i < sql.Length) + { + if (char.IsWhiteSpace(sql[i])) { i++; continue; } + if (sql.AsSpan(i).StartsWith("--")) + { + var end = sql.IndexOf('\n', i); + i = end < 0 ? sql.Length : end + 1; + continue; + } + if (sql.AsSpan(i).StartsWith("/*")) + { + var end = sql.IndexOf("*/", i + 2, StringComparison.Ordinal); + i = end < 0 ? sql.Length : end + 2; + continue; + } + break; + } + + var start = i; + while (i < sql.Length && char.IsLetter(sql[i])) i++; + return i > start ? sql[start..i] : null; + } + /// /// Oracle is checked with DBMS_SQL.PARSE rather than by preparing. /// - /// ODP.NET's Prepare is a client-side no-op — Oracle validates when a statement is - /// executed, not when it is prepared — so the shared check passed everything here, including a + /// ODP.NET's Prepare is a client-side no-op - Oracle validates when a statement is + /// executed, not when it is prepared - so the shared check passed everything here, including a /// select from a table that does not exist. A statement reported as verified without being /// looked at is worse than no check, because it is the one nobody goes back to. /// /// PARSE is the real thing: it compiles the statement and resolves every name in it, raising /// ORA-00942 for a missing table and ORA-00936 for a syntax error, and it runs nothing. Bind - /// placeholders need no values — parsing is exactly the step before binding. + /// placeholders need no values - parsing is exactly the step before binding. /// protected override async Task CheckSyntaxAsync(DbConnection connection, string sql) { + // Except for DDL: Oracle executes a DDL statement the moment it is parsed, so checking + // "drop table orders" would drop it, from a Save or Test button, with no exchange behind it. + if (LeadingKeyword(sql) is { } keyword && DdlKeywords.Contains(keyword)) + throw new InvalidOperationException( + $"{keyword} statements can't be checked on Oracle: parsing one runs it. " + + "Statements are for reading and writing data; run schema changes outside Bitween."); + using var command = connection.CreateCommand(); command.CommandText = @" declare @@ -172,7 +212,7 @@ when others then catch (Exception ex) { // Rethrown without the PL/SQL stack. Running the parse inside a block means the driver - // reports where in OUR block it failed — three ORA-06512 frames through SYS.DBMS_SQL — + // reports where in OUR block it failed - three ORA-06512 frames through SYS.DBMS_SQL - // underneath the one line that is about the operator's SQL. Keeping them would bury // "table does not exist" under the mechanism used to discover it. throw new InvalidOperationException(FirstOracleError(ex.Message), ex); @@ -215,7 +255,7 @@ static string FirstOracleError(string message) IsolationLevels = ["ReadCommitted", "Serializable"], // Array binding exists in ODP.NET, but as a different call shape rather than a flag on this - // path — so it is honestly false until BulkLoad is implemented for it. + // path - so it is honestly false until BulkLoad is implemented for it. BulkCopy = false, Merge = true, Returning = true, @@ -232,13 +272,13 @@ static string FirstOracleError(string message) ReceiveModes = ["bulk", "incrementing", "timestamp", "timestamp+incrementing", "marker"], // `fetch first N rows only`, since 12c. `rownum` is the older idiom and is a trap with an - // ORDER BY — it is applied before the sort, so it takes an arbitrary N and then sorts those. + // ORDER BY - it is applied before the sort, so it takes an arbitrary N and then sorts those. LimitStyle = "fetchFirst" }; /// /// What this login can actually do. Read from the session's own privileges rather than assumed - /// from the engine — an integration account is usually granted a narrow slice, and finding that + /// from the engine - an integration account is usually granted a narrow slice, and finding that /// out at configure time is worth a round trip. /// protected override async Task> ProbePrivilegesAsync(DbConnection connection, @@ -259,7 +299,7 @@ protected override async Task> ProbePrivilegesAsync(DbConnection co } catch (Exception ex) { - // A login without SELECT on session_privs is unusual but not broken — it just cannot + // A login without SELECT on session_privs is unusual but not broken - it just cannot // tell us what it can do. Saying so beats failing the connection test over it. Logger.LogDebug(ex, "Could not read session_privs."); privileges.Add($"(could not be read: {ex.Message})"); @@ -306,7 +346,7 @@ protected override void AddOutParameter(DbCommand command, DbRoutineParameter de /// /// Read from the data dictionary rather than through GetSchema. ODP.NET's collections /// cover tables and columns, but not comments, not routine parameter direction, and not - /// estimated row counts — and the ALL_* views answer all of it in one query per object type. + /// estimated row counts - and the ALL_* views answer all of it in one query per object type. /// /// ALL_, never DBA_: ALL_ is what this login can see, which is the honest answer and the one /// that does not need a privilege an integration account should not have. @@ -380,7 +420,7 @@ from all_objects o Comment = reader.IsDBNull(3) ? null : reader.GetString(3), // From the optimiser's statistics, so it is as fresh as the last gather. Stated - // as an estimate everywhere it surfaces — the alternative is COUNT(*) on a + // as an estimate everywhere it surfaces - the alternative is COUNT(*) on a // stranger's table, which is not a thing a menu should do. RowCount = !request.IncludeRowCounts || reader.IsDBNull(4) ? null @@ -525,7 +565,7 @@ from all_arguments a DbType = dataType, // Surfaced as RefCursor rather than Out, because a caller has to declare it - // differently — this is the field that tells them to. + // differently - this is the field that tells them to. Direction = dataType == "REF CURSOR" ? "RefCursor" : DirectionOf(inOut), Ordinal = reader.IsDBNull(3) ? 0 : Convert.ToInt32(reader.GetValue(3)) }); @@ -578,7 +618,7 @@ from all_sequences s }; /// - /// What a value of this column arrives as in a result row. Coarse on purpose — it is here so a + /// What a value of this column arrives as in a result row. Coarse on purpose - it is here so a /// mapper author knows whether to expect a string or a number, not to be a type system. /// static string ClrTypeOf(string oracleType) => oracleType switch diff --git a/SW.Bitween.Adapters.Db.Oracle/OracleOptions.cs b/SW.Bitween.Adapters.Db.Oracle/OracleOptions.cs index 0bcf48fe..11472643 100644 --- a/SW.Bitween.Adapters.Db.Oracle/OracleOptions.cs +++ b/SW.Bitween.Adapters.Db.Oracle/OracleOptions.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.Adapters.Db.Oracle; /// /// Every setting here arrives as a DataSource property, bound by name, and the form an operator -/// fills in is generated from these attributes — so a field added here appears in Bitween with no +/// fills in is generated from these attributes - so a field added here appears in Bitween with no /// front-end change. /// /// Oracle is the awkward one to configure, and the hints carry that weight deliberately: service @@ -24,13 +24,13 @@ public class OracleOptions : DbOptionsBase public int Port { get; set; } = 1521; [AdapterSetting(Hint = - "The SERVICE name — what `lsnrctl services` lists, e.g. FREEPDB1 or ORCLPDB1. This is what a " + "The SERVICE name - what `lsnrctl services` lists, e.g. FREEPDB1 or ORCLPDB1. This is what a " + "modern Oracle wants. Leave empty only if you are connecting by SID instead.")] public string ServiceName { get; set; } [AdapterSetting(Hint = "The SID, for an older instance that has no service name. Set exactly one of ServiceName " - + "and Sid — a connection naming both is rejected rather than silently preferring one.")] + + "and Sid - a connection naming both is rejected rather than silently preferring one.")] public string Sid { get; set; } [AdapterSetting(Hint = diff --git a/SW.Bitween.Adapters.Db.Oracle/SW.Bitween.Adapters.Db.Oracle.csproj b/SW.Bitween.Adapters.Db.Oracle/SW.Bitween.Adapters.Db.Oracle.csproj index 111cbd23..efb6e8e9 100644 --- a/SW.Bitween.Adapters.Db.Oracle/SW.Bitween.Adapters.Db.Oracle.csproj +++ b/SW.Bitween.Adapters.Db.Oracle/SW.Bitween.Adapters.Db.Oracle.csproj @@ -2,13 +2,13 @@ Exe - net8.0 + net10.0 SW.Bitween.Adapters.Db.Oracle disable - + + + diff --git a/SW.Bitween.Adapters.Db.PostgreSql/adapter.json b/SW.Bitween.Adapters.Db.PostgreSql/adapter.json new file mode 100644 index 00000000..3f936ba5 --- /dev/null +++ b/SW.Bitween.Adapters.Db.PostgreSql/adapter.json @@ -0,0 +1,7 @@ +{ + "id": "bitween.db.postgresql", + "version": "1.0.0", + "displayName": "PostgreSQL", + "summary": "A PostgreSQL database as a data source: its named statements, read from and written to.", + "lifecycle": "resident" +} diff --git a/SW.Bitween.Adapters.Db.SqlServer/SW.Bitween.Adapters.Db.SqlServer.csproj b/SW.Bitween.Adapters.Db.SqlServer/SW.Bitween.Adapters.Db.SqlServer.csproj index 5a1650eb..f8b0ed63 100644 --- a/SW.Bitween.Adapters.Db.SqlServer/SW.Bitween.Adapters.Db.SqlServer.csproj +++ b/SW.Bitween.Adapters.Db.SqlServer/SW.Bitween.Adapters.Db.SqlServer.csproj @@ -2,13 +2,13 @@ Exe - net8.0 + net10.0 SW.Bitween.Adapters.Db.SqlServer disable - + + true + + + + + + + + + """); + + yield return ("adapter.json", $$""" + { + "id": "{{id}}", + "version": "0.1.0", + "displayName": "{{Scaffolder.Spaced(name)}}", + "summary": "What this {{kind}} does, in one sentence, for the adapter list.", + "lifecycle": "classic" + } + """); + + yield return ("Program.cs", Program(name, kind)); + + yield return ("settings.example.json", """ + { + "BaseUrl": "https://partner.example.test", + "ApiKey": "put a test key here, and keep this file out of version control once it holds one" + } + """); + + yield return (".gitignore", """ + bin/ + obj/ + settings.json + """); + + yield return ("README.md", $$""" + # {{Scaffolder.Spaced(name)}} + + A Bitween {{kind}} adapter. + + ```sh + bitween adapter build # builds bin/serverless/{{id}}-0.1.0.zip + cp settings.example.json settings.json # then fill in real values + bitween adapter test --settings settings.json # checks it against the Bitween contract + bitween adapter publish bin/serverless/{{id}}-0.1.0.zip # to the Bitween you signed in to + ``` + + Settings are declared in code with `Runner.Expect`; `bitween adapter build` writes them into + the manifest Bitween reads. + """); + } + + static string Program(string name, string kind) => kind switch + { + "receiver" => $$""" + using SW.Bitween.Adapters; + using SW.Serverless.Sdk; + + namespace {{name}}; + + /// Fetches files on a schedule. Bitween calls Initialize, ListFiles, then GetFile and DeleteFile for each file, then Finalize. + [AdapterKind("receiver")] + [AdapterContract("bitween", 1)] + public class Receiver : IBitweenReceiver + { + public Receiver() + { + // Declare settings here; read them in the methods, never in the constructor. + Runner.Expect("BaseUrl", "https://partner.example.test", description: "Where files are fetched from."); + Runner.Expect("ApiKey", isPrivate: true, description: "The partner's key."); + } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => + Task.FromResult>(new[] { "example-1" }); + + public Task GetFile(string fileId) => + Task.FromResult(new ExchangeFile("{\"id\":\"" + fileId + "\"}", fileId + ".json")); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; + } + + static class Program + { + static Task Main() => Runner.Run(new Receiver()); + } + """, + "validator" => $$""" + using SW.Bitween.Adapters; + using SW.Serverless.Sdk; + + namespace {{name}}; + + /// Checks a message before Bitween accepts it. + [AdapterKind("validator")] + [AdapterContract("bitween", 1)] + public class Validator : IBitweenValidator + { + public Validator() + { + Runner.Expect("MaxBytes", "1000000", description: "The largest message accepted."); + } + + public Task Validate(ExchangeFile file) + { + var result = new ValidationResult(); + if (file.Data.Length > Runner.StartupValueOf("MaxBytes")) + result.AddError("Data", "The message is larger than allowed."); + return Task.FromResult(result); + } + } + + static class Program + { + static Task Main() => Runner.Run(new Validator()); + } + """, + var handlerOrMapper => $$""" + using SW.Bitween.Adapters; + using SW.Serverless.Sdk; + + namespace {{name}}; + + /// {{(handlerOrMapper == "mapper" ? "Maps a message into the shape the next step expects." : "Delivers a message and returns the partner's response.")}} + [AdapterKind("{{handlerOrMapper}}")] + [AdapterContract("bitween", 1)] + public class {{(handlerOrMapper == "mapper" ? "Mapper : IBitweenMapper" : "Handler : IBitweenHandler")}} + { + public {{(handlerOrMapper == "mapper" ? "Mapper" : "Handler")}}() + { + // Declare settings here; read them in the methods, never in the constructor. + Runner.Expect("BaseUrl", "https://partner.example.test", description: "Where messages go."); + Runner.Expect("ApiKey", isPrivate: true, description: "The partner's key."); + } + + public Task Handle(ExchangeFile file) + { + // {{(handlerOrMapper == "mapper" ? "Return the message in its new shape." : "Send file.Data to the partner. A rejection is returned with BadData set, not thrown.")}} + return Task.FromResult(new ExchangeFile(file.Data, file.Filename)); + } + } + + static class Program + { + static Task Main() => Runner.Run(new {{(handlerOrMapper == "mapper" ? "Mapper" : "Handler")}}()); + } + """, + }; + + static IEnumerable<(string File, string Content)> PythonFiles(string name, string id, string kind) + { + yield return ("adapter.json", $$""" + { + "id": "{{id}}", + "version": "0.1.0", + "displayName": "{{Scaffolder.Spaced(name)}}", + "summary": "What this {{kind}} does, in one sentence, for the adapter list.", + "runtime": "python", + "entry": "main.py" + } + """); + + yield return ("main.py", PythonMain(name, kind)); + + yield return ("requirements.txt", $$""" + # What the adapter imports, pinned, one per line; bitween adapter build vendors them into the + # package. The two SDKs are vendored by bitween adapter build itself: they're listed here for + # your editor and for running the tests outside a build. + sw-serverless=={{Scaffolder.PythonSdkVersion}} + simplyworks-bitween>={{BitweenAdaptersPackageVersion}} + """); + + yield return ("settings.example.json", """ + { + "BaseUrl": "https://partner.example.test", + "ApiKey": "put a test key here, and keep this file out of version control once it holds one" + } + """); + + yield return (".gitignore", """ + __pycache__/ + .venv/ + bin/ + settings.json + """); + + yield return ("README.md", $$""" + # {{Scaffolder.Spaced(name)}} + + A Bitween {{kind}} adapter in Python (3.12 or later). + + ```sh + bitween adapter build # builds bin/serverless/{{id}}-0.1.0.zip + cp settings.example.json settings.json # then fill in real values + bitween adapter test --settings settings.json # checks it against the Bitween contract + bitween adapter publish bin/serverless/{{id}}-0.1.0.zip # to the Bitween you signed in to + ``` + + Settings are declared in code with `sw.expect`; `bitween adapter build` writes them into the + manifest Bitween reads. Dependencies go in `requirements.txt`, pinned. + """); + } + + static string PythonMain(string name, string kind) => kind switch + { + "receiver" => $$"""" + import sw_serverless as sw + from simplyworks_bitween import ExchangeFile, Receiver + + + class {{name}}(Receiver): + """Fetches files on a schedule. Bitween calls initialize, list_files, then get_file and + delete_file for each file, then finalize.""" + + def __init__(self): + # Declare settings here; read them in the methods with sw.value_of. + sw.expect("BaseUrl", "https://partner.example.test", description="Where files are fetched from.") + sw.expect("ApiKey", secret=True, description="The partner's key.") + + def list_files(self) -> list[str]: + return ["example-1"] + + def get_file(self, file_id: str) -> ExchangeFile: + return ExchangeFile(data='{"id": "%s"}' % file_id, filename=file_id + ".json") + + def delete_file(self, file_id: str) -> None: + pass + + + if __name__ == "__main__": + sw.run({{name}}) + """", + "validator" => $$"""" + import sw_serverless as sw + from simplyworks_bitween import ExchangeFile, ValidationResult, Validator + + + class {{name}}(Validator): + """Checks a message before Bitween accepts it.""" + + def __init__(self): + sw.expect("MaxBytes", "1000000", type="number", description="The largest message accepted.") + + def validate(self, file: ExchangeFile) -> ValidationResult: + result = ValidationResult() + if len(file.data) > int(sw.value_of("MaxBytes")): + result.add("Data", "The message is larger than allowed.") + return result + + + if __name__ == "__main__": + sw.run({{name}}) + """", + "mapper" => $$"""" + import sw_serverless as sw + from simplyworks_bitween import ExchangeFile, Mapper + + + class {{name}}(Mapper): + """Maps a message into the shape the next step expects.""" + + def map(self, file: ExchangeFile) -> ExchangeFile: + # Return the message in its new shape. + return ExchangeFile(data=file.data, filename=file.filename) + + + if __name__ == "__main__": + sw.run({{name}}) + """", + _ => $$"""" + import sw_serverless as sw + from simplyworks_bitween import ExchangeFile, Handler + + + class {{name}}(Handler): + """Delivers a message and returns the partner's response.""" + + def __init__(self): + # Declare settings here; read them in the methods with sw.value_of. + sw.expect("BaseUrl", "https://partner.example.test", description="Where messages go.") + sw.expect("ApiKey", secret=True, description="The partner's key.") + + def handle(self, file: ExchangeFile) -> ExchangeFile: + # Send file.data to the partner. A rejection is returned with bad_data=True, not raised. + return ExchangeFile(data=file.data, filename=file.filename) + + + if __name__ == "__main__": + sw.run({{name}}) + """", + }; + + static IEnumerable<(string File, string Content)> NodeFiles(string name, string id, string kind, bool typeScript) + { + var entry = typeScript ? "main.ts" : "main.js"; + yield return ("adapter.json", $$""" + { + "id": "{{id}}", + "version": "0.1.0", + "displayName": "{{Scaffolder.Spaced(name)}}", + "summary": "What this {{kind}} does, in one sentence, for the adapter list.", + "runtime": "node", + "entry": "{{entry}}" + } + """); + + yield return (entry, NodeMain(name, kind, typeScript)); + + // The SDKs are named for the editor and for running outside a build; bitween adapter build + // vendors the copies it carries. Other dependencies go here too, and are installed into + // the package. + yield return ("package.json", Scaffolder.NodePackageJson(id, typeScript, + new Dictionary { ["@simplyworks/bitween"] = ">=" + BitweenAdaptersPackageVersion })); + + if (typeScript) + yield return ("tsconfig.json", Scaffolder.TsConfig); + + yield return ("settings.example.json", """ + { + "BaseUrl": "https://partner.example.test", + "ApiKey": "put a test key here, and keep this file out of version control once it holds one" + } + """); + + yield return (".gitignore", """ + node_modules/ + bin/ + settings.json + """); + + yield return ("README.md", $$""" + # {{Scaffolder.Spaced(name)}} + + A Bitween {{kind}} adapter in {{(typeScript ? "TypeScript" : "JavaScript")}}, for Node 22 or later. + + ```sh + bitween adapter build # builds bin/serverless/{{id}}-0.1.0.zip + cp settings.example.json settings.json # then fill in real values + bitween adapter test --settings settings.json # checks it against the Bitween contract + bitween adapter publish bin/serverless/{{id}}-0.1.0.zip # to the Bitween you signed in to + ``` + + Settings are declared in code with `expect`; `bitween adapter build` writes them into the + manifest Bitween reads.{{(typeScript ? " The build strips the types with Node itself, so no compiler is needed; `tsc --noEmit` checks them." : "")}} + """); + } + + static string NodeMain(string name, string kind, bool ts) + { + var header = ts + ? $$""" + import { expect, run, valueOf } from "@simplyworks/sw-serverless"; + import { ExchangeFile, {{Pascal(kind)}}{{(kind == "validator" ? ", ValidationResult" : "")}} } from "@simplyworks/bitween"; + """ + : $$""" + const { expect, run, valueOf } = require("@simplyworks/sw-serverless"); + const { ExchangeFile, {{Pascal(kind)}}{{(kind == "validator" ? ", ValidationResult" : "")}} } = require("@simplyworks/bitween"); + """; + string T(string type) => ts ? type : ""; + var body = kind switch + { + "receiver" => $$""" + /** Fetches files on a schedule. Bitween calls initialize, listFiles, then getFile and deleteFile for each file, then finalize. */ + class {{name}} extends Receiver { + constructor() { + super(); + // Declare settings here; read them in the methods with valueOf. + expect("BaseUrl", { default: "https://partner.example.test", description: "Where files are fetched from." }); + expect("ApiKey", { secret: true, description: "The partner's key." }); + } + + listFiles(){{T(": string[]")}} { + return ["example-1"]; + } + + getFile(fileId{{T(": string")}}){{T(": ExchangeFile")}} { + return new ExchangeFile({ data: JSON.stringify({ id: fileId }), filename: `${fileId}.json` }); + } + + deleteFile(fileId{{T(": string")}}){{T(": void")}} {} + } + """, + "validator" => $$""" + /** Checks a message before Bitween accepts it. */ + class {{name}} extends Validator { + constructor() { + super(); + expect("MaxBytes", { default: "1000000", type: "number", description: "The largest message accepted." }); + } + + validate(file{{T(": ExchangeFile")}}){{T(": ValidationResult")}} { + const result = new ValidationResult(); + if (file.data.length > Number(valueOf("MaxBytes"))) result.add("Data", "The message is larger than allowed."); + return result; + } + } + """, + "mapper" => $$""" + /** Maps a message into the shape the next step expects. */ + class {{name}} extends Mapper { + map(file{{T(": ExchangeFile")}}){{T(": ExchangeFile")}} { + // Return the message in its new shape. + return new ExchangeFile({ data: file.data, filename: file.filename }); + } + } + """, + _ => $$""" + /** Delivers a message and returns the partner's response. */ + class {{name}} extends Handler { + constructor() { + super(); + // Declare settings here; read them in the methods with valueOf. + expect("BaseUrl", { default: "https://partner.example.test", description: "Where messages go." }); + expect("ApiKey", { secret: true, description: "The partner's key." }); + } + + handle(file{{T(": ExchangeFile")}}){{T(": ExchangeFile")}} { + // Send file.data to valueOf("BaseUrl"). A rejection is returned with badData: true, not thrown. + return new ExchangeFile({ data: file.data, filename: file.filename }); + } + } + """, + }; + // valueOf is used by every template but the mapper; keep the import list honest there. + if (kind == "mapper") header = header.Replace("expect, run, valueOf", "run"); + else if (kind == "receiver") header = header.Replace("expect, run, valueOf", "expect, run"); + return header + "\n\n" + body + "\n\nrun(" + name + ");\n"; + } + + static string Pascal(string kind) => char.ToUpperInvariant(kind[0]) + kind[1..]; + +} diff --git a/SW.Bitween.Adapters.Tooling/SW.Bitween.Adapters.Tooling.csproj b/SW.Bitween.Adapters.Tooling/SW.Bitween.Adapters.Tooling.csproj new file mode 100644 index 00000000..69ec1658 --- /dev/null +++ b/SW.Bitween.Adapters.Tooling/SW.Bitween.Adapters.Tooling.csproj @@ -0,0 +1,24 @@ + + + + + net10.0 + latest + disable + false + + + + + + + + + + + + + + diff --git a/SW.Bitween.Adapters/AdapterContract.cs b/SW.Bitween.Adapters/AdapterContract.cs new file mode 100644 index 00000000..c86b5e0d --- /dev/null +++ b/SW.Bitween.Adapters/AdapterContract.cs @@ -0,0 +1,41 @@ +using System.IO; +using System.Reflection; + +namespace SW.Bitween.Adapters; + +/// The Bitween adapter contract, as the JSON it is written in. +public static class AdapterContract +{ + /// The contract version this package describes. + public const int Version = 1; + + /// The kinds of adapter, as adapter manifests name them. + public static class Kinds + { + /// Delivers a message: . + public const string Handler = "handler"; + /// Transforms a message: . + public const string Mapper = "mapper"; + /// Checks a message: . + public const string Validator = "validator"; + /// Fetches files on a schedule: . + public const string Receiver = "receiver"; + } + + /// The contract: kinds, their methods, payload types and encodings. + public static string Json => Read("bitween-adapter-contract.v1.json"); + + /// The JSON Schema of . + public static string ExchangeFileSchema => Read("exchange-file.schema.json"); + + /// The JSON Schema of . + public static string ValidationResultSchema => Read("validation-result.schema.json"); + + private static string Read(string name) + { + using var stream = typeof(AdapterContract).Assembly + .GetManifestResourceStream($"SW.Bitween.Adapters.Contract.{name}")!; + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } +} diff --git a/SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json b/SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json new file mode 100644 index 00000000..a56d1b44 --- /dev/null +++ b/SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json @@ -0,0 +1,121 @@ +{ + "contract": "bitween", + "version": 1, + "description": "What Bitween calls on each kind of adapter, and what it passes. Bitween calls adapters by method name over the SW-Serverless protocol; the names below are exact and case-sensitive.", + "encoding": { + "string": "A string argument or result is the raw UTF-8 text, not a JSON string.", + "object": "Any other argument or result is JSON, with property names exactly as its schema gives them.", + "none": "A method with no argument receives an empty payload; one with no result returns an empty payload." + }, + "types": { + "ExchangeFile": { + "schema": "exchange-file.schema.json" + }, + "ValidationResult": { + "schema": "validation-result.schema.json" + }, + "FileId": { + "encoding": "string", + "description": "An id a receiver gave in ListFiles, passed back exactly as given." + }, + "FileIdList": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "errors": "A call fails when the adapter raises an error; Bitween records the error's type and message on the exchange, and retry policies decide what happens next. A handler whose partner rejected the message does not fail: it returns the partner's answer with BadData true.", + "kinds": { + "handler": { + "description": "Delivers a message and returns the partner's response.", + "methods": [ + { + "name": "Handle", + "input": "ExchangeFile", + "output": "ExchangeFile", + "examples": [ + { + "Data": "{\"orderId\":\"SO-1001\",\"lines\":2}", + "Filename": "order-SO-1001.json", + "BadData": false, + "ContentType": "application/json" + } + ] + } + ] + }, + "mapper": { + "description": "Transforms a message into the shape the next step expects.", + "methods": [ + { + "name": "Handle", + "input": "ExchangeFile", + "output": "ExchangeFile", + "examples": [ + { + "Data": "{\"orderId\":\"SO-1001\",\"lines\":2}", + "Filename": "order-SO-1001.json", + "BadData": false, + "ContentType": "application/json" + } + ] + } + ] + }, + "validator": { + "description": "Checks a message before it is accepted.", + "methods": [ + { + "name": "Validate", + "input": "ExchangeFile", + "output": "ValidationResult", + "examples": [ + { + "Data": "{\"orderId\":\"SO-1001\",\"lines\":2}", + "Filename": "order-SO-1001.json", + "BadData": false, + "ContentType": "application/json" + } + ] + } + ] + }, + "receiver": { + "description": "Fetches files from a source on a schedule.", + "session": "One session per run. Calls come in this order: Initialize, ListFiles, then for each file GetFile and, once it is safely taken in, DeleteFile, and finally Finalize, which is also called after a failure.", + "methods": [ + { + "name": "Initialize", + "input": null, + "output": null + }, + { + "name": "ListFiles", + "input": null, + "output": "FileIdList" + }, + { + "name": "GetFile", + "input": "FileId", + "output": "ExchangeFile" + }, + { + "name": "DeleteFile", + "input": "FileId", + "output": null, + "destructive": true + }, + { + "name": "Finalize", + "input": null, + "output": null + } + ] + } + }, + "examples": "Example inputs for each method that takes one. serverless test calls the adapter with them, using the settings it is given, and checks each answer against the method's output type.", + "destructive": "A destructive method changes the source an adapter reads: serverless test calls it only when told it may." +} diff --git a/SW.Bitween.Adapters/Contract/exchange-file.schema.json b/SW.Bitween.Adapters/Contract/exchange-file.schema.json new file mode 100644 index 00000000..52a5fa06 --- /dev/null +++ b/SW.Bitween.Adapters/Contract/exchange-file.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://bitween.systems/contract/v1/exchange-file.schema.json", + "title": "ExchangeFile", + "description": "A file as Bitween passes it to and from adapters. Property names are exactly as shown; unknown properties are allowed and ignored.", + "type": "object", + "required": ["Data"], + "properties": { + "Filename": { "type": ["string", "null"], "description": "The file's name, when it has one." }, + "Data": { "type": "string", "description": "The content: text, or base64 for binary content." }, + "Hash": { "type": ["string", "null"], "description": "SHA-1 of Data as lower-case hex. Written by .NET adapters; always recomputed from Data when read, never trusted." }, + "BadData": { "type": "boolean", "default": false, "description": "Whether this is a bad response: a delivery the partner rejected." }, + "ContentType": { "type": ["string", "null"], "description": "The content's media type, when known." } + }, + "additionalProperties": true +} diff --git a/SW.Bitween.Adapters/Contract/validation-result.schema.json b/SW.Bitween.Adapters/Contract/validation-result.schema.json new file mode 100644 index 00000000..97d1a839 --- /dev/null +++ b/SW.Bitween.Adapters/Contract/validation-result.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://bitween.systems/contract/v1/validation-result.schema.json", + "title": "ValidationResult", + "description": "What a validator found. Property names are exactly as shown; unknown properties are allowed and ignored.", + "type": "object", + "required": ["Validations"], + "properties": { + "Success": { "type": "boolean", "description": "True when Validations is empty. Bitween derives it from Validations." }, + "Validations": { + "type": "array", + "description": "Each failure: a code, often the field it concerns, and a message.", + "items": { + "type": "object", + "required": ["Key", "Value"], + "properties": { + "Key": { "type": "string" }, + "Value": { "type": "string" } + } + } + } + }, + "additionalProperties": true +} diff --git a/SW.Bitween.Adapters/ExchangeFile.cs b/SW.Bitween.Adapters/ExchangeFile.cs new file mode 100644 index 00000000..8d490643 --- /dev/null +++ b/SW.Bitween.Adapters/ExchangeFile.cs @@ -0,0 +1,57 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SW.Bitween.Adapters; + +/// +/// A file as Bitween passes it to and from adapters: the content, its name, and whether it is a +/// bad response. On the wire it is the JSON {"Filename","Data","Hash","BadData","ContentType"} +/// that SW.PrimitiveTypes.XchangeFile has always produced, so adapters built on either +/// exchange the same thing. +/// +public class ExchangeFile +{ + private string _data = string.Empty; + + /// For deserializers; is empty until set. + public ExchangeFile() + { + } + + /// The content: text, or base64 for binary content. + /// The file's name, when it has one. + /// Whether this is a bad response - a delivery the partner rejected. + public ExchangeFile(string data, string? filename = null, bool badData = false) + { + Data = data; + Filename = filename; + BadData = badData; + } + + /// The file's name, when it has one. + public string? Filename { get; set; } + + /// The content: text, or base64 for binary content. + public string Data + { + get => _data; + set => _data = value ?? throw new ArgumentNullException(nameof(Data), "A file's data can't be null."); + } + + /// SHA-1 of as lower-case hex. Always derived from the data; a value read from JSON is ignored. + public string Hash + { + get + { + using var sha1 = SHA1.Create(); + return BitConverter.ToString(sha1.ComputeHash(Encoding.UTF8.GetBytes(Data))).Replace("-", "").ToLowerInvariant(); + } + } + + /// Whether this is a bad response - a delivery the partner rejected. + public bool BadData { get; set; } + + /// The content's media type, when known. + public string? ContentType { get; set; } +} diff --git a/SW.Bitween.Adapters/Kinds.cs b/SW.Bitween.Adapters/Kinds.cs new file mode 100644 index 00000000..c935a337 --- /dev/null +++ b/SW.Bitween.Adapters/Kinds.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SW.Bitween.Adapters; + +// The .NET form of the Bitween adapter contract (Contract/bitween-adapter-contract.v1.json). Bitween +// calls an adapter by method name, so these names - Handle, Validate, Initialize, ListFiles, +// GetFile, DeleteFile, Finalize - are the contract, and must never change. Adapters written against +// SW.PrimitiveTypes' IInfolinkHandler, IInfolinkValidator and IInfolinkReceiver use the same names +// and are called the same way. + +/// Delivers a message: given the file to send, returns the partner's response. +public interface IBitweenHandler +{ + /// Delivers . A rejection by the partner is returned with set, not thrown. + Task Handle(ExchangeFile file); +} + +/// Transforms a message: given a file, returns it mapped into the shape the next step expects. +public interface IBitweenMapper +{ + /// Returns mapped into the shape the next step expects. + Task Handle(ExchangeFile file); +} + +/// Checks a message before it is accepted, and says what is wrong with it. +public interface IBitweenValidator +{ + /// Checks ; an empty result means it passed. + Task Validate(ExchangeFile file); +} + +/// +/// Fetches files from a source on a schedule. Bitween calls, on one session and in this order: +/// , , then for each file and, +/// once it is safely taken in, , and finally - +/// also after a failure. +/// +public interface IBitweenReceiver +{ + /// Opens the session: connects, signs in. + Task Initialize(); + + /// The ids of the files ready to take in this run. + Task> ListFiles(); + + /// The file with an id gave. + Task GetFile(string fileId); + + /// Removes, or moves aside, a file Bitween has safely taken in. + Task DeleteFile(string fileId); + + /// Closes the session. Called after a failure too. + Task Finalize(); +} diff --git a/SW.Bitween.Adapters/SW.Bitween.Adapters.csproj b/SW.Bitween.Adapters/SW.Bitween.Adapters.csproj new file mode 100644 index 00000000..6897d60c --- /dev/null +++ b/SW.Bitween.Adapters/SW.Bitween.Adapters.csproj @@ -0,0 +1,20 @@ + + + + + netstandard2.0 + latest + enable + SimplyWorks.Bitween.Adapters + The Bitween adapter contract: the kinds of adapter Bitween runs (handler, mapper, validator, receiver), what it calls on each, and the payloads it exchanges with them. + Simplify9 + bitween;adapter;integration + true + + + + + + + + diff --git a/SW.Bitween.Adapters/ValidationResult.cs b/SW.Bitween.Adapters/ValidationResult.cs new file mode 100644 index 00000000..346fe4b4 --- /dev/null +++ b/SW.Bitween.Adapters/ValidationResult.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SW.Bitween.Adapters; + +/// +/// What a validator found. On the wire it is the JSON +/// {"Success": bool, "Validations": [{"Key": code, "Value": message}]} that +/// SW.PrimitiveTypes.InfolinkValidatorResult has always produced. +/// +public class ValidationResult +{ + private List> _validations = new(); + + /// A result with no failures. + public ValidationResult() + { + } + + /// A result with these failures. + public ValidationResult(IEnumerable> validations) + { + _validations = new List>(validations); + } + + /// True when nothing failed. + public bool Success => _validations.Count == 0; + + /// Each failure: a code (often the field it concerns) and a message. + public List> Validations + { + get => _validations; + set => _validations = value ?? new List>(); + } + + /// Records one failure. + public ValidationResult AddError(string code, string message) + { + _validations.Add(new KeyValuePair( + code ?? throw new ArgumentNullException(nameof(code)), + message ?? throw new ArgumentNullException(nameof(message)))); + return this; + } + + /// Whether a failure with this code was recorded. + public bool Has(string code) => _validations.Any(v => v.Key == code); +} diff --git a/SW.Bitween.Api/Controllers/AdapterPackagesController.cs b/SW.Bitween.Api/Controllers/AdapterPackagesController.cs new file mode 100644 index 00000000..b3567227 --- /dev/null +++ b/SW.Bitween.Api/Controllers/AdapterPackagesController.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Controllers; + +/// +/// Publishing a package through Bitween: what bitween adapter publish does by default, so the people +/// who write adapters need a Bitween account rather than the storage's keys, and every version +/// published is in the audit trail with who published it. +/// +/// +/// A controller rather than a CqApi handler because the package is the request body itself - the zip +/// bytes, as built - not JSON. Refusals answer as CqApi's do: 403 without the permission, 400 with +/// { field: [message] }. +/// +[ApiController] +[Route("api/adapters/packages")] +public class AdapterPackagesController(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ControllerBase +{ + /// major, minor, patch or an exact version; the package's own when empty. + /// Make it the version that runs where nothing is pinned. + /// Replaces the package's own, when given. + [HttpPost] + [RequestSizeLimit(200 * 1024 * 1024)] + public async Task Post([FromQuery] string version, [FromQuery] bool current, [FromQuery] string releaseNotes) + { + try + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Operate); + } + catch (SWUnauthorizedException) + { + return StatusCode(403); + } + + var who = requestContext.GetNameIdentifier(); + var account = int.TryParse(who, out var accountId) + ? await dbContext.Set().AsNoTracking().FirstOrDefaultAsync(a => a.Id == accountId) + : null; + + try + { + var published = await workshop.UploadAsync(Request.Body, version, current, releaseNotes, account?.DisplayName ?? who); + dbContext.Add(new AdapterRelease(published.Manifest.Id, published.Version, AdapterRelease.PublishedAction, null, who)); + if (current) + dbContext.Add(new AdapterRelease(published.Manifest.Id, published.Version, AdapterRelease.PromotedAction, null, who)); + await dbContext.SaveChangesAsync(); + return Ok(new { AdapterId = published.Manifest.Id, published.Version, Current = current, published.Sha256 }); + } + catch (SWValidationException ex) + { + foreach (var v in ex.Validations) ModelState.AddModelError(v.Key, v.Value); + return BadRequest(ModelState); + } + catch (SWException ex) + { + ModelState.AddModelError("Package", ex.Message); + return BadRequest(ModelState); + } + } +} diff --git a/SW.Bitween.Api/Controllers/FileLinksController.cs b/SW.Bitween.Api/Controllers/FileLinksController.cs index 760dc732..3b739436 100644 --- a/SW.Bitween.Api/Controllers/FileLinksController.cs +++ b/SW.Bitween.Api/Controllers/FileLinksController.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Controllers; /// the public storage URL it stands in for worked, so a reader built for those links needs no change. /// /// -/// The literal "files" keeps this clear of CqApi, which owns the rest of /api/ — see +/// The literal "files" keeps this clear of CqApi, which owns the rest of /api/ - see /// . /// [ApiController] diff --git a/SW.Bitween.Api/Controllers/GatewayController.cs b/SW.Bitween.Api/Controllers/GatewayController.cs index da321abb..9e4fb061 100644 --- a/SW.Bitween.Api/Controllers/GatewayController.cs +++ b/SW.Bitween.Api/Controllers/GatewayController.cs @@ -21,11 +21,13 @@ public class GatewayController( RequestContext requestContext, IInfolinkCache cache, XchangeService xchangeService, - GatewayCallers callers) : ControllerBase + GatewayCallers callers, + GatewayActivity activity, + BitweenOptions options) : ControllerBase { /// /// {gateway's url name}/sync or /async. The url name can be several segments, - /// so it is everything before the last one — a route can't put a literal after a catch-all. + /// so it is everything before the last one - a route can't put a literal after a catch-all. /// /// /// The literal "gateway" is what keeps this clear of CqApi, which owns the rest of /api/ with @@ -50,45 +52,68 @@ public Task Post([FromRoute] string path) private async Task ProcessAsync(string gatewayApiName, bool resultSync) { var globalAdapterValuesSet = await cache.ListGlobalAdapterValuesSetsAsync(); - // Lowered on the column too: rows saved before names had to be lowercase may not be. + // The exact name first, which the unique index on UrlName answers. Lowering the column for + // every call made each one a scan of the table; it is kept as the fallback only, for rows + // saved before names had to be lowercase. var apiGateway = await dbContext.Set() .Include(ag => ag.Partners) .ThenInclude(agp => agp.Partner) - .FirstOrDefaultAsync(ag => ag.UrlName.ToLower() == gatewayApiName); + .FirstOrDefaultAsync(ag => ag.UrlName == gatewayApiName) + ?? await dbContext.Set() + .Include(ag => ag.Partners) + .ThenInclude(agp => agp.Partner) + .FirstOrDefaultAsync(ag => ag.UrlName.ToLower() == gatewayApiName); + var address = HttpContext.Connection.RemoteIpAddress?.ToString(); if (apiGateway == null) + { + activity.Refused(gatewayApiName, "unknown-gateway", 404, address); return NotFound(); + } // Resolve the partner the way this gateway asks callers to prove who they are - var (authorized, partner, callerReference) = await callers.Identify(apiGateway, requestContext); + var (authorized, partner, callerReference, keyName) = await callers.Identify(apiGateway, requestContext); if (!authorized) + { + activity.Refused(gatewayApiName, "not-authenticated", 401, address); return Unauthorized(); + } // Verify partner is part of the API Gateway var apiGatewayPartner = apiGateway.Partners.FirstOrDefault(agp => agp.PartnerId == partner.Id); if (apiGatewayPartner == null) + { + activity.Refused(gatewayApiName, "not-attached", 401, address); return Unauthorized(); + } + await activity.KeyUsedAsync(partner.Id, keyName); // After authorisation on purpose: whether a gateway exists and is switched off is - // something only an attached partner should learn — checking it earlier would + // something only an attached partner should learn - checking it earlier would // answer that for anyone who guessed the url. // // 503 rather than 404 because the url is right and the partner should keep it: a // 404 reads as "wrong address" and sends someone hunting for a new one, where this // is a gateway somebody switched off and will switch back on. if (apiGateway.Inactive) + { + activity.Refused(gatewayApiName, "gateway-off", 503, address); return StatusCode(StatusCodes.Status503ServiceUnavailable, $"The '{apiGateway.Name}' gateway is currently deactivated."); + } var subscription = await cache.SubscriptionByIdAsync(apiGatewayPartner.SubscriptionId); if (subscription == null) + { + activity.Refused(gatewayApiName, "no-subscription", 404, address); return NotFound(); + } var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); - // The file name travels with the exchange into handlers, some of which write it to disk — + // The file name travels with the exchange into handlers, some of which write it to disk - // a slash there is a directory nobody asked for. JSON and XML types keep the ".json" // they have always had, since whatever picks those files up may expect it. var document = await cache.DocumentByIdAsync(subscription.DocumentId); @@ -100,40 +125,72 @@ private async Task ProcessAsync(string gatewayApiName, bool resul }; var xchangeFile = new XchangeFile(json, $"{gatewayApiName.Replace('/', '-')}{extension}"); + // Told now, while the partner is still on the line, rather than as a failed exchange later. + var schemaErrors = document is null + ? [] + : await DocumentSchema.Check(document.DocumentFormat, document.ValidationSchema, json); + if (schemaErrors.Count > 0) + return BadRequest(new { error = "SCHEMA_MISMATCH", errors = schemaErrors }); + var validatorProperties = subscription.ValidatorProperties.ToDictionary() .Fill(partner, globalAdapterValuesSet); - await xchangeService.RunValidator(subscription.ValidatorId, validatorProperties, + await xchangeService.RunValidator(subscription.ValidatorRef, validatorProperties, xchangeFile); var xchangeReferences = new List { callerReference }; - var globalAdapterValuesSets = await dbContext.Set().ToArrayAsync(); - var xchangeId = await xchangeService.SubmitSubscriptionXchange(subscription.Id, xchangeFile, - xchangeReferences.ToArray(), partner, globalAdapterValuesSets); - if (!resultSync) + + // A repeat of a call the partner couldn't be sure arrived - same Idempotency-Key - gets the + // exchange the first one made, not a second. Scoped to this gateway and this partner. + var idempotencyKey = Request.Headers[IdempotencyGuard.Header].FirstOrDefault()?.Trim(); + if (idempotencyKey?.Length > IdempotencyGuard.MaxKeyLength) + return BadRequest($"{IdempotencyGuard.Header} is longer than {IdempotencyGuard.MaxKeyLength} characters."); + var idempotencyScope = $"gateway:{apiGateway.Id}:partner:{partner.Id}"; + + var xchangeId = string.IsNullOrEmpty(idempotencyKey) + ? null + : await IdempotencyGuard.FindAsync(dbContext, idempotencyScope, idempotencyKey); + if (xchangeId is not null) { - return Accepted(xchangeId); + Response.Headers["Idempotent-Replay"] = "true"; } - - var waitResponse = 120; - // check headers for wait response value - var waitResponseHeader = Request.Headers["Wait-Period"].FirstOrDefault(); - if (int.TryParse(waitResponseHeader, out var waitResponseValue)) + else { - waitResponse = waitResponseValue <= 0 ? 120 : waitResponseValue; + // The sets already read at the top of the request, from the cache. + var xchange = await xchangeService.CreateXchange(subscription, xchangeFile, xchangeReferences.ToArray(), + Guid.NewGuid().ToString("N"), partner, globalAdapterValuesSet); + if (!string.IsNullOrEmpty(idempotencyKey)) + await IdempotencyGuard.StageAsync(dbContext, idempotencyScope, idempotencyKey, xchange.Id); + + try + { + await dbContext.SaveChangesAsync(); + xchangeId = xchange.Id; + } + catch (DbUpdateException ex) when (IdempotencyGuard.IsDuplicateKey(ex)) + { + // Another call with the same key committed first: its exchange is the answer. + dbContext.ChangeTracker.Clear(); + xchangeId = await IdempotencyGuard.FindAsync(dbContext, idempotencyScope, idempotencyKey); + Response.Headers["Idempotent-Replay"] = "true"; + } } - var currentFibTerm = 1; - var previousTerm = 1; - while (currentFibTerm <= waitResponse) + if (!resultSync) { - await Task.Delay(TimeSpan.FromSeconds(currentFibTerm)); - var nextTerm = Math.Min(currentFibTerm + previousTerm, 8); - previousTerm = currentFibTerm; - currentFibTerm = nextTerm; - if (!await dbContext.Set() - .AsNoTracking() - .AnyAsync(i => i.Id == xchangeId)) continue; + return Accepted(xchangeId); + } + // The caller may ask for its own wait in Wait-Period; it is capped, see ResultWait. + var waitResponse = ResultWait.Clamp( + int.TryParse(Request.Headers["Wait-Period"].FirstOrDefault(), out var asked) ? asked : null, + options.MaxResponseWaitSeconds); + + var available = await ResultWait.UntilAsync( + () => dbContext.Set().AsNoTracking().AnyAsync(i => i.Id == xchangeId), + waitResponse, HttpContext.RequestAborted); + + if (available) + { var xchangeResult = await dbContext.FindAsync(xchangeId); @@ -154,7 +211,17 @@ await xchangeService.RunValidator(subscription.ValidatorId, validatorProperties, }; } case false: - return BadRequest(); + { + if (apiGateway.FailureAnswer == GatewayFailureAnswer.Legacy) return BadRequest(); + // The retry, if any, is scheduled in the same save as the result, so it is there now. + var retryOn = await dbContext.Set().AsNoTracking() + .Where(r => r.Id == xchangeId).Select(r => (DateTime?)r.On).FirstOrDefaultAsync(); + var hasMapper = await dbContext.Set().AsNoTracking() + .Where(x => x.Id == xchangeId).Select(x => x.MapperId != null).FirstOrDefaultAsync(); + var answer = GatewayFailureAnswers.For(apiGateway.FailureAnswer, xchangeId, hasMapper, + mapped: xchangeResult.OutputSize > 0 || xchangeResult.OutputName != null, retryOn); + return new ObjectResult(answer.Body) { StatusCode = answer.Status }; + } } } diff --git a/SW.Bitween.Api/Controllers/GatewayFailureAnswers.cs b/SW.Bitween.Api/Controllers/GatewayFailureAnswers.cs new file mode 100644 index 00000000..07a91318 --- /dev/null +++ b/SW.Bitween.Api/Controllers/GatewayFailureAnswers.cs @@ -0,0 +1,40 @@ +using System; +using SW.Bitween.Model; + +namespace SW.Bitween.Controllers; + +/// +/// What a sync gateway call is told when the delivery behind it failed. Legacy says 400 whatever +/// happened, which tells the caller its request was wrong: in the lab, orders refused by a warehouse +/// that was down were answered 400 and then delivered by retry minutes later, so the marketplace was +/// told "rejected" about orders that went through. Accurate says what Bitween will do next. +/// +public static class GatewayFailureAnswers +{ + public record Answer(int Status, object Body); + + /// Whether the subscription maps the message before delivering it. + /// Whether mapping produced an output, so the failure came after it. + /// When the retry Bitween scheduled is due; null when there is none. + public static Answer For(GatewayFailureAnswer mode, string exchangeId, bool hasMapper, bool mapped, DateTime? retryOn) + { + if (mode == GatewayFailureAnswer.Legacy) return new Answer(400, null); + + // Bitween has the message and will deliver it again: sending it again would make a second one. + if (retryOn is { } next) + return new Answer(202, new { exchangeId, status = "retrying", nextAttemptOn = next }); + + if (hasMapper && !mapped) + return new Answer(422, new + { + exchangeId, status = "failed", + error = "The message couldn't be mapped to what the destination takes. It won't be retried.", + }); + + return new Answer(502, new + { + exchangeId, status = "failed", + error = "The delivery to the destination failed. It won't be retried.", + }); + } +} diff --git a/SW.Bitween.Api/Controllers/XchangeExportController.cs b/SW.Bitween.Api/Controllers/XchangeExportController.cs index 394c1708..64ca4b7f 100644 --- a/SW.Bitween.Api/Controllers/XchangeExportController.cs +++ b/SW.Bitween.Api/Controllers/XchangeExportController.cs @@ -22,7 +22,7 @@ namespace SW.Bitween.Controllers; /// /// The files of several exchanges as one zip, for the exchange list's "Export files": one folder per /// exchange, named like its archive ({main value}_{id}), holding input, mapped -/// and handled — whichever it has. says beforehand how many exchanges are +/// and handled - whichever it has. says beforehand how many exchanges are /// older than storage keeps their files, so the person hears it before choosing to export. Files missing /// anyway, or for another reason, are listed in missing.txt, so one of them never costs the rest. /// @@ -33,8 +33,8 @@ namespace SW.Bitween.Controllers; /// {prefix}/{resource}/{token}, the way 's does. /// /// -/// Refusals answer the way CqApi does — 401 without the permission, 400 with { code: [message] } -/// — so the client reads them like any other call. They're all decided before the first byte is sent. +/// Refusals answer the way CqApi does - 401 without the permission, 400 with { code: [message] } +/// - so the client reads them like any other call. They're all decided before the first byte is sent. /// /// [ApiController] @@ -44,7 +44,7 @@ public class XchangeExportController(BitweenDbContext dbContext, RequestContext : ControllerBase { /// - /// The most exchanges one zip takes — the bulk retry's ceiling, for the same reason: every file is + /// The most exchanges one zip takes - the bulk retry's ceiling, for the same reason: every file is /// read from storage in sequence, inside the one request. /// public const int Limit = 500; @@ -102,6 +102,11 @@ public async Task Post([FromBody] XchangeFilesExport request) var missing = ids.Except(xchanges.Select(x => x.Id)) .Select(id => $"{id}: this exchange no longer exists.").ToList(); + // Exchange files leave Bitween here, so who took which is recorded before they do. + await dbContext.RecordNowAsync(requestContext, OperatorAction.Export, + xchanges.Count == 1 ? xchanges[0].Id : $"{xchanges.Count} exchanges", + string.Join(", ", xchanges.Take(20).Select(x => x.Id)) + (xchanges.Count > 20 ? ", …" : "")); + Response.ContentType = "application/zip"; Response.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { diff --git a/SW.Bitween.Api/Data/AuditPolicy.cs b/SW.Bitween.Api/Data/AuditPolicy.cs index 27fe0a9e..48734d77 100644 --- a/SW.Bitween.Api/Data/AuditPolicy.cs +++ b/SW.Bitween.Api/Data/AuditPolicy.cs @@ -34,8 +34,8 @@ public static class AuditPolicy { /// /// The configuration entities, their owned children, and the account/role records. Runtime - /// traffic — every Xchange, result, delivery, receive attempt, retry usage counter and - /// refresh token — is deliberately absent. + /// traffic - every Xchange, result, delivery, receive attempt, retry usage counter and + /// refresh token - is deliberately absent. /// private static readonly HashSet Audited = [ @@ -57,13 +57,23 @@ public static class AuditPolicy typeof(Setting), typeof(Account), typeof(Role), - typeof(AccountRoleLink) + typeof(AccountRoleLink), + // Issued and revoked: who made a credential, and who ended it. + typeof(ServiceToken), + // Not configuration, but reading an adapter's source is what the trail is for. + typeof(AdapterSourceAccess), + // Code written in Bitween, and what was published and made current from it. + typeof(AdapterDraft), + typeof(AdapterRelease), + // Retries, exports, requeues and queue deletions: not configuration, but who did them is + // what the trail is asked. + typeof(OperatorAction) ]; /// /// Properties whose values never enter the trail. The adapter property bags are here because /// each is stored as a single JSON column, so the change tracker sees one property rather than - /// the individual keys — there is no way to keep Host and drop Password without + /// the individual keys - there is no way to keep Host and drop Password without /// cracking the JSON open and asking the adapter which of its keys are [Secure], which /// would mean starting a serverless adapter in the middle of a save. Recording that a /// subscription was edited without saying which adapter key changed is the deliberate trade. @@ -89,7 +99,14 @@ public static class AuditPolicy // The key itself is the credential. Name survives, so adding or revoking one is visible. [typeof(ApiCredential)] = [nameof(ApiCredential.Key)], - [typeof(Account)] = [nameof(Account.Password)] + [typeof(Account)] = [nameof(Account.Password)], + + // A hash of a random token, so no use to anyone; kept out anyway, like a partner's key. + [typeof(ServiceToken)] = [nameof(ServiceToken.Hash)], + + // Not a secret, but the code would be in the trail twice on every save. FilesHash is + // recorded instead: each save shows, and the code itself is in the published package. + [typeof(AdapterDraft)] = [nameof(AdapterDraft.FilesJson)] }; public static readonly AuditOptions Options = new() @@ -105,7 +122,7 @@ private static bool ShouldAuditProperty(EntityEntry entry, IProperty property) return false; // A setting's value is only sometimes a secret, and unlike an adapter's properties the - // catalog says which — a static lookup, no adapter to start — so the theme colour stays + // catalog says which - a static lookup, no adapter to start - so the theme colour stays // readable in the trail while the licence key never enters it. if (entry.Entity is Setting setting && property.Name == nameof(Setting.Value)) return SettingsCatalog.Find(setting.Id)?.Secret != true; diff --git a/SW.Bitween.Api/Data/BitweenDbContext.cs b/SW.Bitween.Api/Data/BitweenDbContext.cs index f50b5cf7..1096c746 100644 --- a/SW.Bitween.Api/Data/BitweenDbContext.cs +++ b/SW.Bitween.Api/Data/BitweenDbContext.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System; using Microsoft.EntityFrameworkCore; using SW.EfCoreExtensions; @@ -21,7 +22,7 @@ public class BitweenDbContext(DbContextOptions options, RequestContext requestCo private readonly IPublish publish = publish; // Parsed as Unspecified kind, so the .ToUniversalTime() calls at every use site below used - // to convert using whatever timezone the current machine happened to be in — deterministic + // to convert using whatever timezone the current machine happened to be in - deterministic // on any one developer's machine, but different on every other machine/CI runner, which // permanently disagreed with whichever machine last regenerated the migration snapshot. protected readonly DateTime defaultCreatedOn = DateTime.SpecifyKind(DateTime.Parse("1/1/2022"), DateTimeKind.Utc); @@ -130,7 +131,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ds.Property(p => p.Name).IsRequired().HasMaxLength(200); ds.Property(p => p.AdapterId).IsRequired().HasMaxLength(200).IsUnicode(false); ds.Property(p => p.Kind).HasConversion(); - ds.Property(p => p.Properties).StoreAsJson(); + ds.Property(p => p.Properties).StoreAsJson().Encrypted(); ds.Property(p => p.SecretProperties).StoreAsJson(); ds.Property(p => p.LastKnownState).HasMaxLength(100).IsUnicode(false); ds.Property(p => p.OwnedByNode).HasMaxLength(200).IsUnicode(false); @@ -146,7 +147,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) st.Property(p => p.Sql).IsRequired(); st.Property(p => p.Description).HasMaxLength(1000); - // Column names, so the database's own identifier limit is the ceiling — 128 is + // Column names, so the database's own identifier limit is the ceiling - 128 is // above every engine's (Oracle allows 128, PostgreSQL 63). st.Property(p => p.CursorColumn).HasMaxLength(128).IsUnicode(false); st.Property(p => p.KeyColumn).HasMaxLength(128).IsUnicode(false); @@ -180,7 +181,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // Declared here rather than only in the PgSql context: leader election needs this table // on every provider Bitween supports, and a node whose database has no cluster_lease - // cannot fence anything — which means two nodes can consume one queue, silently. + // cannot fence anything - which means two nodes can consume one queue, silently. modelBuilder.Entity(cl => { cl.ToTable("ClusterLeases"); @@ -194,7 +195,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) im.ToTable("InboundMessages"); // The dedupe key IS the key. A unique constraint the database enforces is the - // whole mechanism — see the type's remarks. + // whole mechanism - see the type's remarks. im.HasKey(i => i.Id); im.Property(i => i.Id).HasMaxLength(400).IsUnicode(false); im.Property(i => i.XchangeId).HasMaxLength(50).IsUnicode(false); @@ -225,7 +226,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) gav.ToTable("GlobalAdapterValuesSets"); gav.HasKey(i => i.Id); gav.Property(p => p.Id).IsUnicode(false).HasMaxLength(200); - gav.Property(p => p.Values).StoreAsJson(); + gav.Property(p => p.Values).StoreAsJson().Encrypted(); gav.Property(p => p.SecretProperties).StoreAsJson(); }); modelBuilder.Entity(b => @@ -233,7 +234,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.ToTable("Partners"); b.Metadata.SetNavigationAccessMode(PropertyAccessMode.Field); b.Property(p => p.Name).IsRequired().IsUnicode(false).HasMaxLength(200); - b.Property(p => p.AdapterProperties).StoreAsJson(); + b.Property(p => p.AdapterProperties).StoreAsJson().Encrypted(); b.Property(p => p.SecretProperties).StoreAsJson(); b.Property(p => p.LoginIdentity).HasMaxLength(500); b.HasIndex(p => p.LoginIdentity).IsUnique(); @@ -244,6 +245,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) apicred.ToTable("PartnerApiCredentials"); apicred.Property(p => p.Name).IsRequired().HasMaxLength(500); apicred.Property(p => p.Key).IsRequired().IsUnicode(false).HasMaxLength(500); + apicred.Property(p => p.KeyPrefix).HasMaxLength(10); apicred.HasIndex(p => p.Key).IsUnique(); apicred.WithOwner().HasForeignKey("PartnerId"); @@ -276,16 +278,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) schedules.Property(p => p.Recurrence).HasConversion(); }); - b.Property(p => p.HandlerProperties).StoreAsJson(); - b.Property(p => p.MapperProperties).StoreAsJson(); - b.Property(p => p.ReceiverProperties).StoreAsJson(); - b.Property(p => p.ValidatorProperties).StoreAsJson(); + b.Property(p => p.HandlerProperties).StoreAsJson().Encrypted(); + b.Property(p => p.MapperProperties).StoreAsJson().Encrypted(); + b.Property(p => p.ReceiverProperties).StoreAsJson().Encrypted(); + b.Property(p => p.ValidatorProperties).StoreAsJson().Encrypted(); b.Property(p => p.DocumentFilter).StoreAsJson(); b.Property(p => p.ResponseMessageTypeName).IsUnicode(false).HasMaxLength(500); b.Property(p => p.MapperId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); + // Pinned versions, null to follow whatever is current. See SubscriptionConfiguration.HandlerVersion. + b.Property(p => p.ReceiverVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.ValidatorVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.MapperVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.HandlerVersion).HasMaxLength(50).IsUnicode(false); b.Property(p => p.ReceiverId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.ValidatorId).HasMaxLength(200).IsUnicode(false); @@ -306,6 +313,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasOne(i => i.RetryPolicy).WithMany().HasForeignKey(i => i.RetryPolicyId).IsRequired(false) .OnDelete(DeleteBehavior.SetNull); b.Property(p => p.CustomRetryPolicy).StoreAsJson(); + b.Property(p => p.ExpectTraffic).StoreAsJson(); b.Property(p => p.MatchExpression).HasMatchExpressionConversion(); }); @@ -317,7 +325,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Name).IsRequired().HasMaxLength(200); b.Property(p => p.Groups).StoreAsJson(); b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); - b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + b.Property(p => p.AlertHandlerProperties).StoreAsJson().Encrypted(); }); modelBuilder.Entity(b => @@ -329,6 +337,132 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(p => p.On); }); + modelBuilder.Entity(b => + { + b.ToTable("IdempotencyKeys"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.Scope).IsRequired().HasMaxLength(200); + b.Property(p => p.Key).IsRequired().HasMaxLength(200); + b.Property(p => p.XchangeId).IsRequired().IsUnicode(false).HasMaxLength(50); + b.HasIndex(p => new { p.Scope, p.Key }).IsUnique(); + b.HasIndex(p => p.CreatedOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("OutboxMessages"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.MessageType).IsRequired().HasMaxLength(500); + b.Property(p => p.Body).IsRequired(); + b.Property(p => p.Headers).StoreAsJson(); + b.Property(p => p.LastError).HasMaxLength(2000); + b.HasIndex(p => p.PublishedOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("OperatorActions"); + b.HasKey(a => a.Id); + b.Property(a => a.Id).ValueGeneratedOnAdd(); + b.Property(a => a.Action).HasMaxLength(50).IsRequired(); + b.Property(a => a.Target).HasMaxLength(500); + b.Property(a => a.Detail).HasMaxLength(2000); + b.Property(a => a.AccountId).HasMaxLength(50); + b.HasIndex(a => a.OccurredOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ApiKeyUses"); + b.HasKey(u => new { u.PartnerId, u.KeyName }); + b.Property(u => u.KeyName).HasMaxLength(500); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ClusterNodes"); + b.Property(p => p.Id).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.Host).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.Runtimes).HasMaxLength(100).IsUnicode(false); + b.Property(p => p.ResidentHealth).HasMaxLength(4000); + b.HasIndex(p => p.LastSeenOn); + }); + + // Metrics: hourly rollups each node adds to once a minute, and broker snapshots. Keys lead + // with the hour, so reading a time range and pruning old hours both use the key itself. + modelBuilder.Entity(b => + { + b.ToTable("ExchangeStats"); + b.HasKey(p => new { p.Hour, p.Node, p.SubscriptionId, p.PartnerId }); + b.Property(p => p.Node).HasMaxLength(100).IsUnicode(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AdapterStats"); + b.HasKey(p => new { p.Hour, p.Node, p.AdapterId, p.Version, p.Role, p.Runtime }); + b.Property(p => p.Node).HasMaxLength(100).IsUnicode(false); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.Role).HasMaxLength(20).IsUnicode(false); + b.Property(p => p.Runtime).HasMaxLength(20).IsUnicode(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ErrorStats"); + b.HasKey(p => new { p.Hour, p.Node, p.Scope, p.ScopeKey, p.Fingerprint }); + b.Property(p => p.Node).HasMaxLength(100).IsUnicode(false); + b.Property(p => p.Scope).HasMaxLength(20).IsUnicode(false); + b.Property(p => p.ScopeKey).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.Fingerprint).HasMaxLength(16).IsUnicode(false); + b.Property(p => p.Sample).HasMaxLength(500); + }); + + modelBuilder.Entity(b => + { + b.ToTable("QueueSnapshots"); + b.HasKey(p => p.At); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AdapterDrafts"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Language).HasMaxLength(20).IsUnicode(false).IsRequired(); + b.Property(p => p.Kind).HasMaxLength(20).IsUnicode(false); + b.Property(p => p.BaseVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.FilesJson).IsRequired(); + b.Property(p => p.FilesHash).HasMaxLength(64).IsUnicode(false).IsRequired(); + b.Property(p => p.CreatedBy).HasMaxLength(200); + b.Property(p => p.ModifiedBy).HasMaxLength(200); + b.HasIndex(p => p.AdapterId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AdapterReleases"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false).IsRequired(); + b.Property(p => p.Action).HasMaxLength(20).IsUnicode(false).IsRequired(); + b.Property(p => p.AccountId).HasMaxLength(100).IsUnicode(false); + b.HasIndex(p => new { p.AdapterId, p.OccurredOn }); + }); + + modelBuilder.Entity(b => + { + b.ToTable("AdapterSourceAccesses"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.Path).HasMaxLength(500); + b.Property(p => p.AccountId).HasMaxLength(100).IsUnicode(false); + b.HasIndex(p => new { p.AdapterId, p.OccurredOn }); + }); + modelBuilder.Entity(b => { b.ToTable("ReceiveAttempts"); @@ -353,7 +487,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasKey(p => new { p.SubscriptionId, p.GroupId }); b.Property(p => p.AlertMode).HasConversion(); b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); - b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + b.Property(p => p.AlertHandlerProperties).StoreAsJson().Encrypted(); }); modelBuilder.Entity(b => @@ -366,8 +500,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.InputName).HasMaxLength(200); b.Property(p => p.MapperId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); - b.Property(p => p.HandlerProperties).StoreAsJson(); - b.Property(p => p.MapperProperties).StoreAsJson(); + b.Property(p => p.HandlerProperties).StoreAsJson().Encrypted(); + b.Property(p => p.MapperProperties).StoreAsJson().Encrypted(); b.Property(p => p.SourceValues).StoreAsJson(); b.Property(p => p.SourceXchangeId).IsUnicode(false).HasMaxLength(50); b.Property(p => p.InputContentType).IsUnicode(false).HasMaxLength(200); @@ -407,6 +541,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.RetryGroupId); b.Property(p => p.AttemptNumber); b.HasIndex(p => p.RetryGroupId); + b.Property(p => p.NodeName).HasMaxLength(100).IsUnicode(false); + b.Property(p => p.MapperVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.HandlerVersion).HasMaxLength(50).IsUnicode(false); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); @@ -419,6 +556,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.XchangeId).IsUnicode(false).HasMaxLength(50); // Retention deletes an exchange's notifications by exchange id. b.HasIndex(p => p.XchangeId); + // The quiet window looks up a notifier's recent sends. + b.HasIndex(p => new { p.NotifierId, p.FinishedOn }); }); modelBuilder.Entity(b => @@ -454,7 +593,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // No b-tree on PropertiesRaw. The only query on it is the Exchanges page's substring // search, a leading-wildcard LIKE no b-tree can serve (Postgres has a trigram index // for it, PromotedPropertiesTrigramIndex). Having one made long values fail the - // save — after the delivery, so the retry delivered again — and it is what had + // save - after the delivery, so the retry delivered again - and it is what had // capped the column at 450 characters on SQL Server and 255 on MySQL. b.HasOne().WithOne().HasForeignKey(p => p.Id) @@ -466,7 +605,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.ToTable("Notifiers"); b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).HasMaxLength(100).IsRequired(); - b.Property(p => p.HandlerProperties).StoreAsJson(); + b.Property(p => p.HandlerProperties).StoreAsJson().Encrypted(); b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.RunOnSubscriptions).IsSeparatorDelimited(); }); @@ -484,6 +623,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.EmailProvider).HasConversion(); b.Property(p => p.LoginMethods).HasConversion(); + // Added with service accounts; every account before them is a person (0). + b.Property(p => p.Kind).HasConversion(); b.HasData( new @@ -498,6 +639,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Password = defaultPasswordHash, Deleted = false, Role = AccountRole.Admin, + Kind = AccountKind.Person, FailedLoginCount = 0, // True for a fresh installation, whose password is the published default. // Installations that already exist are handled by the migration, which @@ -517,6 +659,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.LoginMethod).HasConversion(); }); + modelBuilder.Entity(b => + { + b.ToTable("ServiceTokens"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasOne().WithMany().HasForeignKey(p => p.AccountId).OnDelete(DeleteBehavior.Cascade); + + b.Property(p => p.Name).IsRequired().HasMaxLength(100); + b.Property(p => p.Hash).IsRequired().IsUnicode(false).HasMaxLength(64); + b.Property(p => p.Shown).IsRequired().IsUnicode(false).HasMaxLength(20); + b.Property(p => p.RevokedBy).HasMaxLength(200); + b.Property(p => p.LastUsedFrom).IsUnicode(false).HasMaxLength(64); + // Every sign-in looks a token up by its hash. + b.HasIndex(p => p.Hash).IsUnique(); + }); + modelBuilder.Entity(b => { b.ToTable("Roles"); @@ -548,7 +706,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Id).IsUnicode(false).HasMaxLength(200); }); - // ——— Audit trail ——— + // --- Audit trail --- // A table that only ever grows and is only ever appended to. The indexes answer the two // questions asked of it: the history of one row, and everything one save changed. modelBuilder.Entity(b => @@ -571,8 +729,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } /// - /// The built-in roles. Their grants aren't stored — - /// derives them from the catalog — so adding a permission never needs a data fix-up here. + /// The built-in roles. Their grants aren't stored - + /// derives them from the catalog - so adding a permission never needs a data fix-up here. /// protected Role[] SystemRoleSeed() => [ @@ -589,21 +747,21 @@ protected Role[] SystemRoleSeed() => /// /// Refused, because it would save without auditing. Only - /// captures the trail, and an audit table with a silent hole in it is worse than none — + /// captures the trail, and an audit table with a silent hole in it is worse than none - /// nobody would know which changes it had missed. Nothing in Bitween calls this today; /// this makes sure a future caller finds out immediately rather than quietly. /// public override int SaveChanges() => throw new NotSupportedException( - "Use SaveChangesAsync — the synchronous path would skip the audit trail."); + "Use SaveChangesAsync - the synchronous path would skip the audit trail."); /// /// Saves, and records what was saved. The audit rows are written inside the same transaction - /// as the change they describe, so the trail can never disagree with the data — a save that + /// as the change they describe, so the trail can never disagree with the data - a save that /// rolls back takes its audit rows with it. /// /// - /// The transaction is opened only when there is something to audit. Runtime traffic — every - /// xchange, result and receive attempt — is excluded by and so + /// The transaction is opened only when there is something to audit. Runtime traffic - every + /// xchange, result and receive attempt - is excluded by and so /// still saves in exactly one round trip, unchanged. When a caller has already opened a /// transaction of its own, that one is used rather than nested. /// @@ -614,6 +772,25 @@ async public override Task SaveChangesAsync(CancellationToken cancellationT var pendingAudit = ChangeTracker.CapturePendingAuditDiffs(userId, AuditPolicy.Options); + // Domain events go into the outbox in the same save as the change that raised them, so + // one can no longer be committed without the other. They are taken off the entities + // only once the save has succeeded; a save that fails leaves them where they were. + var raised = ChangeTracker.Entries() + .Select(e => e.Entity) + .Where(e => e.Events.Any()) + .ToArray(); + var fromEvents = raised + .SelectMany(entity => entity.Events) + .Select(ToOutboxMessage) + .ToList(); + foreach (var message in fromEvents) Add(message); + + // Plus any a caller queued itself, such as a delivery's response. + var outgoing = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added) + .Select(e => e.Entity) + .ToList(); + var transaction = pendingAudit.Count > 0 && Database.CurrentTransaction is null ? await Database.BeginTransactionAsync(cancellationToken) : null; @@ -631,39 +808,106 @@ async public override Task SaveChangesAsync(CancellationToken cancellationT Add(new AuditEntry(diff)); // base, deliberately: re-entering this override would audit the audit rows and - // publish every domain event a second time. + // write every domain event to the outbox a second time. await base.SaveChangesAsync(cancellationToken); } if (transaction is not null) await transaction.CommitAsync(cancellationToken); } + catch + { + // Not saved, so not to be published - and not left tracked, or the next save + // would insert them alongside a second copy made from the same events. + foreach (var message in outgoing) Entry(message).State = EntityState.Detached; + throw; + } finally { if (transaction is not null) await transaction.DisposeAsync(); } - // Published after the commit. An event announcing a change that then rolled back would - // send every consumer after a row that never existed. - //await ChangeTracker.PublishDomainEvents(publish); - var entitiesWithEvents = ChangeTracker.Entries() - .Select(e => e.Entity) - .Where(e => e.Events.Any()) - .ToArray(); + foreach (var entity in raised) entity.Events.Clear(); + + // Published straight after the commit, as before; the outbox is for when that fails. + // Inside a caller's transaction nothing is committed yet - announcing a row that may + // still roll back would send every consumer after a row that never existed - so those + // wait for the dispatcher. + if (outgoing.Count > 0 && Database.CurrentTransaction is null) + await PublishOutboxAsync(outgoing, cancellationToken); + + return affectedRecords; + } - foreach (var entity in entitiesWithEvents) + /// + /// Saves without an audit entry or an outbox message - for rewriting stored values in a new + /// form, which changes nothing a person did. Only + /// should need it. + /// + internal Task SaveRewrittenValuesAsync(CancellationToken cancellationToken) => + base.SaveChangesAsync(cancellationToken); + + private static OutboxMessage ToOutboxMessage(object domainEvent) => + domainEvent is IHasWorkGroup hasWorkGroup + ? new OutboxMessage(hasWorkGroup.GetBusMessageName(), + JsonConvert.SerializeObject(new XchangeMessage { Id = hasWorkGroup.Id })) + : new OutboxMessage(domainEvent.GetType().Name, JsonConvert.SerializeObject(domainEvent)); + + /// + /// Publishes saved outbox rows and marks those the broker took. One that fails stays unpublished, + /// with the error on it, for . Never throws: the change is + /// committed, and failing the caller now would only make it try the change a second time. + /// + /// How many the broker took. + internal async Task PublishOutboxAsync(IReadOnlyCollection messages, + CancellationToken cancellationToken = default) + { + var published = new List(); + foreach (var message in messages) { - var events = entity.Events.ToArray(); - entity.Events.Clear(); - foreach (var domainEvent in events) - if (domainEvent is IHasWorkGroup hasWorkGroup) - await publish.Publish(hasWorkGroup.GetBusMessageName(), - JsonConvert.SerializeObject(new XchangeMessage { Id = hasWorkGroup.Id })); + try + { + // Bitween's bus publisher takes values as well; anything that only publishes + // (a test double) gets the message without them. + if (message.Headers is { Count: > 0 } && publish is SW.Bus.RabbitMqExtensions.IPublishWithValues withValues) + await withValues.Publish(message.MessageType, message.Body, message.Headers); else - await publish.Publish(domainEvent.GetType().Name, JsonConvert.SerializeObject(domainEvent)); + await publish.Publish(message.MessageType, message.Body); + published.Add(message.Id); + } + catch (Exception ex) + { + var error = ex.Message.Length > 2000 ? ex.Message[..2000] : ex.Message; + try + { + await Set().Where(m => m.Id == message.Id) + .ExecuteUpdateAsync(u => u + .SetProperty(m => m.Attempts, m => m.Attempts + 1) + .SetProperty(m => m.LastError, error), CancellationToken.None); + } + catch + { + // The dispatcher finds it either way; the error text is only a courtesy. + } + } } + if (published.Count == 0) return 0; - return affectedRecords; + var now = DateTime.UtcNow; + try + { + await Set().Where(m => published.Contains(m.Id)) + .ExecuteUpdateAsync(u => u + .SetProperty(m => m.PublishedOn, now) + .SetProperty(m => m.Attempts, m => m.Attempts + 1), CancellationToken.None); + } + catch + { + // Published but not marked: the dispatcher will publish them again. Consumers take a + // repeat - Process checks for an existing result before doing anything. + } + + return published.Count; } } } diff --git a/SW.Bitween.Api/Data/SecretColumnCipher.cs b/SW.Bitween.Api/Data/SecretColumnCipher.cs new file mode 100644 index 00000000..ac6a40ac --- /dev/null +++ b/SW.Bitween.Api/Data/SecretColumnCipher.cs @@ -0,0 +1,182 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; + +namespace SW.Bitween; + +/// +/// Encrypts the columns that hold credentials - partner and global values, data source settings, +/// the adapter properties of subscriptions, notifiers and alerts, and the copies of them each +/// exchange keeps - so a copy of the database, or a backup, is not a copy of every password. +/// +/// +/// +/// The whole column is encrypted, not the values marked secret. Adapters published as packages +/// rarely declare which of their properties are secret, so encrypting only declared ones would +/// leave exactly those exposed. +/// +/// +/// Keyed by Bitween:SettingsEncryptionKey, the passphrase the settings secrets already use. +/// Without it nothing is encrypted and nothing changes. A stored value carries the enc:v1: +/// prefix, so plaintext written before this - or by a deployment with no key - still reads. To +/// change the key, move the old one to Bitween:PreviousSettingsEncryptionKey: values are read +/// with either and written with the new one. +/// +/// +public static class SecretColumnCipher +{ + public const string Prefix = "enc:v1:"; + + private const int NonceBytes = 12; + private const int TagBytes = 16; + + // Derived once, not per value: these columns are read on every exchange, and the per-value + // PBKDF2 the settings use would cost a hundred thousand rounds each time. + private static readonly byte[] Salt = Encoding.UTF8.GetBytes("bitween/secret-columns/v1"); + + private static byte[] _key; + private static byte[] _previousKey; + + /// Whether values are being encrypted as they are written. + public static bool Enabled => _key is not null; + + public static void Configure(string passphrase, string previousPassphrase = null) + { + _key = string.IsNullOrWhiteSpace(passphrase) ? null : Derive(passphrase); + _previousKey = string.IsNullOrWhiteSpace(previousPassphrase) ? null : Derive(previousPassphrase); + } + + public static string Protect(string plaintext) + { + if (_key is null || plaintext is null || plaintext.StartsWith(Prefix, StringComparison.Ordinal)) + return plaintext; + + var data = Encoding.UTF8.GetBytes(plaintext); + var output = new byte[NonceBytes + data.Length + TagBytes]; + var nonce = output.AsSpan(0, NonceBytes); + RandomNumberGenerator.Fill(nonce); + + using var aes = new AesGcm(_key, TagBytes); + aes.Encrypt(nonce, data, output.AsSpan(NonceBytes, data.Length), output.AsSpan(NonceBytes + data.Length)); + return Prefix + Convert.ToBase64String(output); + } + + public static string Unprotect(string stored) + { + if (stored is null || !stored.StartsWith(Prefix, StringComparison.Ordinal)) + return stored; + + var input = Convert.FromBase64String(stored[Prefix.Length..]); + foreach (var key in new[] { _key, _previousKey }) + { + if (key is null) continue; + try + { + return Decrypt(key, input); + } + catch (AuthenticationTagMismatchException) + { + // Not this key; try the previous one. + } + } + + throw new InvalidOperationException( + "A stored value is encrypted with a key this instance does not have. Set " + + "Bitween:SettingsEncryptionKey to the key it was written with, or put that key in " + + "Bitween:PreviousSettingsEncryptionKey while moving to a new one."); + } + + private static string Decrypt(byte[] key, byte[] input) + { + var length = input.Length - NonceBytes - TagBytes; + var plaintext = new byte[length]; + using var aes = new AesGcm(key, TagBytes); + aes.Decrypt(input.AsSpan(0, NonceBytes), input.AsSpan(NonceBytes, length), + input.AsSpan(NonceBytes + length), plaintext); + return Encoding.UTF8.GetString(plaintext); + } + + private static byte[] Derive(string passphrase) => + Rfc2898DeriveBytes.Pbkdf2(passphrase, Salt, 210_000, HashAlgorithmName.SHA256, 32); + + /// + /// Wraps the converter already on the property - StoreAsJson, typically - so the JSON it + /// produces is encrypted on the way to the database and decrypted on the way back. + /// + public static PropertyBuilder Encrypted(this PropertyBuilder builder) + { + var inner = builder.Metadata.GetValueConverter() + ?? throw new InvalidOperationException( + $"{builder.Metadata.Name} has no converter to encrypt the output of; call StoreAsJson first."); + var comparer = builder.Metadata.GetValueComparer(); + + var cipher = new ValueConverter(v => Protect(v), v => Unprotect(v)); + builder.Metadata.SetValueConverter(inner.ComposeWith(cipher)); + if (comparer is not null) builder.Metadata.SetValueComparer(comparer); + return builder; + } + + /// + /// The same for a PostgreSQL jsonb column, which must always hold valid JSON. The + /// encrypted payload is stored as a JSON string, so the column stays valid; a column written + /// before encryption - a plain JSON object - still reads. + /// + public static PropertyBuilder EncryptedJsonb(this PropertyBuilder builder) where T : class + { + builder.HasConversion( + new ValueConverter(v => ToJsonb(v), s => FromJsonb(s)), + new ValueComparer( + (a, b) => JsonConvert.SerializeObject(a) == JsonConvert.SerializeObject(b), + v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(), + v => v == null ? null : JsonConvert.DeserializeObject(JsonConvert.SerializeObject(v)))); + builder.HasColumnType("jsonb"); + return builder; + } + + private static string ToJsonb(T value) + { + if (value is null) return null; + var json = JsonConvert.SerializeObject(value); + return Enabled ? JsonConvert.SerializeObject(Protect(json)) : json; + } + + private static T FromJsonb(string stored) where T : class + { + if (stored is null) return null; + var json = stored.TrimStart().StartsWith("\"" + Prefix, StringComparison.Ordinal) + ? Unprotect(JsonConvert.DeserializeObject(stored)) + : stored; + return JsonConvert.DeserializeObject(json); + } + + /// + /// For a key-value column (PostgreSQL hstore) rather than JSON: each value is encrypted + /// on its own, and the keys stay readable. + /// + public static PropertyBuilder> EncryptedValues( + this PropertyBuilder> builder) + { + builder.HasConversion( + new ValueConverter, System.Collections.Generic.Dictionary>( + v => MapValues(v, true), v => MapValues(v, false)), + new ValueComparer>( + (a, b) => JsonConvert.SerializeObject(a) == JsonConvert.SerializeObject(b), + v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(), + v => v == null ? null : new System.Collections.Generic.Dictionary(v))); + return builder; + } + + private static System.Collections.Generic.Dictionary MapValues( + System.Collections.Generic.Dictionary values, bool protect) + { + if (values is null) return null; + var result = new System.Collections.Generic.Dictionary(values.Count); + foreach (var (key, value) in values) result[key] = protect ? Protect(value) : Unprotect(value); + return result; + } +} diff --git a/SW.Bitween.Api/Domain/Accounts/Account.cs b/SW.Bitween.Api/Domain/Accounts/Account.cs index eadb08fe..8ab87939 100644 --- a/SW.Bitween.Api/Domain/Accounts/Account.cs +++ b/SW.Bitween.Api/Domain/Accounts/Account.cs @@ -17,6 +17,23 @@ public Account(string displayName, string email, string password, AccountRole ro Role = role; } + /// + /// An account for a program rather than a person: no email, no password, nothing to sign + /// in to the admin UI with. It holds roles like anyone, and signs in with its tokens. + /// + public static Account ForService(string name, AccountRole role) => new() + { + DisplayName = name, + Role = role, + Kind = AccountKind.Service, + LoginMethods = LoginMethod.ApiKey, + }; + + /// A person unless created as a service account; every account before these existed is a person. + public AccountKind Kind { get; private set; } + + public bool IsService => Kind == AccountKind.Service; + public string Email { get; private set; } public string DisplayName { get; set; } @@ -44,6 +61,15 @@ public Account(string displayName, string email, string password, AccountRole ro /// public bool MustChangePassword { get; private set; } + /// + /// The Microsoft identity - object id and tenant id - this account signs in as, bound on its + /// first Microsoft sign-in. Matching by address alone let anyone who could get Microsoft to + /// put that address in a token sign in as the account. + /// + public string MicrosoftIdentity { get; private set; } + + public void BindMicrosoftIdentity(string identity) => MicrosoftIdentity = identity; + public bool IsLockedOut(DateTime nowUtc) => LockoutEnd.HasValue && LockoutEnd.Value > nowUtc; public void RegisterSuccessfulLogin() @@ -52,6 +78,14 @@ public void RegisterSuccessfulLogin() LockoutEnd = null; } + /// + /// When the member last signed in with a password, Microsoft or the CLI; not when a session + /// renewed itself. Being on the audited Account, each sign-in is in the audit trail too. + /// + public DateTime? LastSignInOn { get; private set; } + + public void SignedIn() => LastSignInOn = DateTime.UtcNow; + // Admin action: clear a lockout before it expires. public void Unlock() { diff --git a/SW.Bitween.Api/Domain/Accounts/AccountKind.cs b/SW.Bitween.Api/Domain/Accounts/AccountKind.cs new file mode 100644 index 00000000..14e76e14 --- /dev/null +++ b/SW.Bitween.Api/Domain/Accounts/AccountKind.cs @@ -0,0 +1,13 @@ +namespace SW.Bitween.Domain.Accounts; + +/// Who an account is: a person who signs in, or a program that holds service tokens. +public enum AccountKind +{ + Person = 0, + + /// + /// Signs in only by trading one of its s; never with a password, + /// Microsoft or the CLI's browser sign-in, and never into the admin UI. + /// + Service = 1 +} diff --git a/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs b/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs index acc1047a..489773fe 100644 --- a/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs +++ b/SW.Bitween.Api/Domain/Accounts/AccountRoleLink.cs @@ -1,6 +1,6 @@ namespace SW.Bitween.Domain.Accounts; -/// Which roles an account holds. Composite key — one row per account/role pair. +/// Which roles an account holds. Composite key - one row per account/role pair. public class AccountRoleLink { private AccountRoleLink() diff --git a/SW.Bitween.Api/Domain/Accounts/RefreshToken.cs b/SW.Bitween.Api/Domain/Accounts/RefreshToken.cs index 7cf11a37..e2317492 100644 --- a/SW.Bitween.Api/Domain/Accounts/RefreshToken.cs +++ b/SW.Bitween.Api/Domain/Accounts/RefreshToken.cs @@ -15,5 +15,12 @@ public RefreshToken(int accountId, LoginMethod loginMethod) public int AccountId { get; private set; } public LoginMethod LoginMethod { get; set; } public DateTime CreatedOn { get ; set ; } + + /// + /// When a refresh replaced this token. It stays usable for a short grace after that: the + /// browser only learns the new token from the response, and a response lost to a navigation + /// would otherwise leave it holding a token that no longer worked - signed out mid-task. + /// + public DateTime? SupersededOn { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/Accounts/Role.cs b/SW.Bitween.Api/Domain/Accounts/Role.cs index 08c6193e..10301102 100644 --- a/SW.Bitween.Api/Domain/Accounts/Role.cs +++ b/SW.Bitween.Api/Domain/Accounts/Role.cs @@ -23,7 +23,7 @@ private Role() { } - /// Seeding path for the built-in roles — fixed Id, grants computed at runtime. + /// Seeding path for the built-in roles - fixed Id, grants computed at runtime. public Role(int id, string name, string description) { Id = id; @@ -43,7 +43,7 @@ public Role(string name, string description, IEnumerable permissions) public string Description { get; private set; } /// - /// Catalog keys, stored as one JSON column. Empty for built-in roles — see + /// Catalog keys, stored as one JSON column. Empty for built-in roles - see /// . /// public List Permissions { get; private set; } = []; @@ -61,7 +61,7 @@ public void Update(string name, string description, IEnumerable permissi /// /// What this role actually grants. Built-in roles derive their grants from the catalog at /// runtime instead of storing them, so adding a permission to the catalog reaches - /// Administrators — and the right groups — without a migration or a data fix-up. + /// Administrators - and the right groups - without a migration or a data fix-up. /// public List GetEffectivePermissions() => IsSystem ? SystemPermissions(Id) : Permissions; diff --git a/SW.Bitween.Api/Domain/Accounts/ServiceToken.cs b/SW.Bitween.Api/Domain/Accounts/ServiceToken.cs new file mode 100644 index 00000000..8949499e --- /dev/null +++ b/SW.Bitween.Api/Domain/Accounts/ServiceToken.cs @@ -0,0 +1,89 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain.Accounts; + +/// +/// A credential a service account signs in with, unattended: a job applying configuration, a +/// pipeline publishing adapters. Traded for an ordinary short session at +/// POST /api/accounts/servicetoken, so everything a session can do is checked the same way. +/// +/// +/// Only a hash is kept; the token itself is shown once, when it is made. Every token expires, and +/// can be revoked on its own or with its account, by disabling it. +/// +public class ServiceToken : BaseEntity, IAudited +{ + public const string Prefix = "bwt_"; + public const int RandomLength = 40; + public const int MaxLifetimeDays = 365; + public const int DefaultLifetimeDays = 90; + + /// Unrevoked, unexpired tokens one account may hold: two, so one can replace the other. + public const int MaxActivePerAccount = 2; + + /// How much of the token is kept to tell tokens apart on screen: the prefix and 8 more. + public const int ShownLength = 12; + + const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + private ServiceToken() + { + } + + ServiceToken(int accountId, string name, string hash, string shown, DateTime expiresOn) + { + AccountId = accountId; + Name = name; + Hash = hash; + Shown = shown; + ExpiresOn = expiresOn; + } + + /// A new token for the account, and the only time its value is known. + public static (ServiceToken Token, string Value) Issue(int accountId, string name, DateTime expiresOn) + { + var value = Prefix + RandomNumberGenerator.GetString(Alphabet, RandomLength); + return (new ServiceToken(accountId, name, HashOf(value), value[..ShownLength], expiresOn), value); + } + + public static string HashOf(string value) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); + + /// Whether a string is shaped like a token at all, before looking it up. + public static bool LooksLikeOne(string value) => + value is { Length: 44 } && value.StartsWith(Prefix, StringComparison.Ordinal); + + public int AccountId { get; private set; } + public string Name { get; private set; } + + /// SHA-256 of the token, hex. The tokens are random, so a plain hash is enough. + public string Hash { get; private set; } + + /// The token's first characters, for telling it apart on screen and in logs. + public string Shown { get; private set; } + + public DateTime ExpiresOn { get; private set; } + public DateTime? RevokedOn { get; private set; } + public string RevokedBy { get; private set; } + + /// When it was last traded for a session, and from which address. Not audited: it moves on every use. + public DateTime? LastUsedOn { get; private set; } + public string LastUsedFrom { get; private set; } + + public bool IsActive(DateTime nowUtc) => RevokedOn is null && ExpiresOn > nowUtc; + + public void Revoke(string by) + { + if (RevokedOn is not null) return; + RevokedOn = DateTime.UtcNow; + RevokedBy = by; + } + + public DateTime CreatedOn { get; set; } + public string CreatedBy { get; set; } + public DateTime? ModifiedOn { get; set; } + public string ModifiedBy { get; set; } +} diff --git a/SW.Bitween.Api/Domain/AdapterDraft.cs b/SW.Bitween.Api/Domain/AdapterDraft.cs new file mode 100644 index 00000000..64a5ca70 --- /dev/null +++ b/SW.Bitween.Api/Domain/AdapterDraft.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// An adapter being written in Bitween's code editor: its files, until they are published as a +/// version. Python and JavaScript/TypeScript only - they build from their source with nothing but +/// their runtime, which the image has; .NET and Go adapters are built with the CLI. +/// +public class AdapterDraft : BaseEntity, IAudited +{ + public static readonly string[] Languages = ["python", "node", "typescript"]; + + /// What a draft may hold, so a draft can't become a way to store anything in the database. + public const int MaxFiles = 200; + public const int MaxBytes = 2 * 1024 * 1024; + + private AdapterDraft() + { + } + + public AdapterDraft(string adapterId, string language, string kind, string baseVersion, IDictionary files) + { + if (string.IsNullOrWhiteSpace(adapterId)) throw new ArgumentException("A draft needs an adapter id.", nameof(adapterId)); + if (!Languages.Contains(language)) throw new SWValidationException("Language", $"'{language}' isn't a language the editor builds: {string.Join(", ", Languages)}."); + AdapterId = adapterId; + Language = language; + Kind = kind; + BaseVersion = baseVersion; + SetFiles(files); + } + + public string AdapterId { get; private set; } + public string Language { get; private set; } + public string Kind { get; private set; } + + /// The version it was started from or last published as; null for a new adapter not yet published. + public string BaseVersion { get; private set; } + + /// The files as JSON, path to content. Kept out of the audit trail; is in it. + public string FilesJson { get; private set; } + + /// SHA-256 of the files, so the trail shows every save without holding the code twice. + public string FilesHash { get; private set; } + + public DateTime CreatedOn { get; set; } + public string CreatedBy { get; set; } + public DateTime? ModifiedOn { get; set; } + public string ModifiedBy { get; set; } + + public Dictionary Files => + JsonConvert.DeserializeObject>(FilesJson ?? "{}") ?? new Dictionary(); + + public void SetFiles(IDictionary files) + { + if (files == null || files.Count == 0) throw new SWValidationException("Files", "A draft needs its files."); + if (files.Count > MaxFiles) throw new SWValidationException("Files", $"A draft holds at most {MaxFiles} files."); + foreach (var path in files.Keys) + if (!IsSafePath(path)) throw new SWValidationException("Files", $"'{path}' isn't a path inside the adapter."); + if (files.Values.Sum(v => Encoding.UTF8.GetByteCount(v ?? "")) > MaxBytes) + throw new SWValidationException("Files", $"A draft holds at most {MaxBytes / (1024 * 1024)} MB of code."); + + var sorted = new SortedDictionary(files.ToDictionary(f => f.Key, f => f.Value ?? ""), StringComparer.Ordinal); + FilesJson = JsonConvert.SerializeObject(sorted); + FilesHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(FilesJson))).ToLowerInvariant(); + } + + public void Published(string version) => BaseVersion = version; + + /// Relative, forward slashes, nothing that climbs out or hides: what a project folder holds. + public static bool IsSafePath(string path) + { + if (string.IsNullOrWhiteSpace(path) || path.Length > 300) return false; + if (path.StartsWith('/') || path.Contains('\\') || path.Contains(':') || path.Contains('\0')) return false; + var segments = path.Split('/'); + return segments.All(s => s.Length > 0 && s != "." && s != ".."); + } +} + +/// A version published or made current from Bitween: in the audit trail, with who did it. +public class AdapterRelease +{ + public const string PublishedAction = "published"; + public const string PromotedAction = "promoted"; + public const string WithdrawnAction = "withdrawn"; + + private AdapterRelease() + { + } + + public AdapterRelease(string adapterId, string version, string action, int? draftId, string accountId) + { + AdapterId = adapterId; + Version = version; + Action = action; + DraftId = draftId; + AccountId = accountId; + OccurredOn = DateTime.UtcNow; + } + + public long Id { get; private set; } + public string AdapterId { get; private set; } + public string Version { get; private set; } + public string Action { get; private set; } + public int? DraftId { get; private set; } + public string AccountId { get; private set; } + public DateTime OccurredOn { get; private set; } +} diff --git a/SW.Bitween.Api/Domain/AdapterSourceAccess.cs b/SW.Bitween.Api/Domain/AdapterSourceAccess.cs new file mode 100644 index 00000000..d809e8e4 --- /dev/null +++ b/SW.Bitween.Api/Domain/AdapterSourceAccess.cs @@ -0,0 +1,36 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// Someone read an adapter's source in Bitween. Kept as a row, rather than only logged, so the +/// audit trail records it as it records every other change: who, which adapter and version, +/// which file, and when. Source is a partner's or our own code, and reading it is worth knowing. +/// +public class AdapterSourceAccess +{ + private AdapterSourceAccess() + { + } + + public AdapterSourceAccess(string adapterId, string version, string path, string accountId) + { + AdapterId = adapterId; + Version = version; + Path = path; + AccountId = accountId; + OccurredOn = DateTime.UtcNow; + } + + public long Id { get; private set; } + public string AdapterId { get; private set; } + + /// The version read; empty for a package published without one. + public string Version { get; private set; } + + /// The file read, relative to the package's source folder; null when only the list was. + public string Path { get; private set; } + + public string AccountId { get; private set; } + public DateTime OccurredOn { get; private set; } +} diff --git a/SW.Bitween.Api/Domain/Audit/AuditEntry.cs b/SW.Bitween.Api/Domain/Audit/AuditEntry.cs index 05bb1c7f..2d848114 100644 --- a/SW.Bitween.Api/Domain/Audit/AuditEntry.cs +++ b/SW.Bitween.Api/Domain/Audit/AuditEntry.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.Domain; /// /// /// Rows are written by for the entities -/// admits, in the same transaction as the change itself — so an entry +/// admits, in the same transaction as the change itself - so an entry /// exists for every audited change that committed, and for no change that didn't. /// /// @@ -53,7 +53,7 @@ public AuditEntry(GenericAuditDiffJson diff) public DateTime OccurredOn { get; private set; } /// - /// The account id behind the change, or null for a save with no signed-in user — a bus consumer + /// The account id behind the change, or null for a save with no signed-in user - a bus consumer /// or a scheduled job. Kept as the id rather than a name so that renaming an account doesn't /// rewrite history. /// diff --git a/SW.Bitween.Api/Domain/Cluster/ClusterLease.cs b/SW.Bitween.Api/Domain/Cluster/ClusterLease.cs index bf1a9dae..b93f4908 100644 --- a/SW.Bitween.Api/Domain/Cluster/ClusterLease.cs +++ b/SW.Bitween.Api/Domain/Cluster/ClusterLease.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.Domain.Cluster; /// /// The fencing token for one exclusively-owned resource. /// -/// This is not the lock — the lock lives on the bus. This is the monotonic counter the bus cannot +/// This is not the lock - the lock lives on the bus. This is the monotonic counter the bus cannot /// provide, so that a node which was paused while ownership moved can discover it lost. /// public class ClusterLease : BaseEntity diff --git a/SW.Bitween.Api/Domain/Cluster/ClusterNode.cs b/SW.Bitween.Api/Domain/Cluster/ClusterNode.cs new file mode 100644 index 00000000..a2361e30 --- /dev/null +++ b/SW.Bitween.Api/Domain/Cluster/ClusterNode.cs @@ -0,0 +1,40 @@ +using System; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain.Cluster; + +/// +/// A Bitween node, as it last said it was: written by every node on a heartbeat, so the cluster +/// can be seen from any of them. Not audited: it is the nodes describing themselves, not +/// configuration anyone changes. +/// +public class ClusterNode : BaseEntity +{ + private ClusterNode() + { + } + + /// The node's name as leases record it: host and process id. + public ClusterNode(string name) + { + Id = name ?? throw new ArgumentNullException(nameof(name)); + } + + public string Host { get; set; } + public DateTime StartedOn { get; set; } + public DateTime LastSeenOn { get; set; } + public string Version { get; set; } + + /// Bitween:BusProvidersEnabled on this node: whether it runs data sources. + public bool DataSources { get; set; } + + /// The adapter runtimes this node has, comma-separated: dotnet,python,node. + public string Runtimes { get; set; } + + /// + /// The resident adapter instances running on this node at its last beat - state, uptime, + /// restarts, quarantine - as JSON, so any node can show every node's. Null on a node without + /// resident adapters, and on beats from before this was recorded. + /// + public string ResidentHealth { get; set; } +} diff --git a/SW.Bitween.Api/Domain/DataSources/AdapterState.cs b/SW.Bitween.Api/Domain/DataSources/AdapterState.cs index 1ff5fb6d..74793f9d 100644 --- a/SW.Bitween.Api/Domain/DataSources/AdapterState.cs +++ b/SW.Bitween.Api/Domain/DataSources/AdapterState.cs @@ -5,7 +5,7 @@ namespace SW.Bitween.Domain.DataSources; /// /// A bookmark a resident adapter asked Bitween to hold for it. /// -/// The motivating case is a polling database receiver's cursor — the last incrementing id or +/// The motivating case is a polling database receiver's cursor - the last incrementing id or /// timestamp it consumed. The adapter cannot keep it: the supervisor restarts it, the next instance /// may come up on another node, and a pooled one is not the same process twice. Any of those resets /// a cursor held in a field back to the beginning, which means replaying every row already @@ -26,7 +26,7 @@ public class AdapterState public string AdapterId { get; set; } /// - /// Which instance owns it — the data source id for an exclusive resident. Part of the key + /// Which instance owns it - the data source id for an exclusive resident. Part of the key /// because two instances of one adapter are two different connections, and one reading the /// other's cursor would skip rows that were never processed. /// diff --git a/SW.Bitween.Api/Domain/DataSources/DataSource.cs b/SW.Bitween.Api/Domain/DataSources/DataSource.cs index d1c5634b..2793ecff 100644 --- a/SW.Bitween.Api/Domain/DataSources/DataSource.cs +++ b/SW.Bitween.Api/Domain/DataSources/DataSource.cs @@ -11,8 +11,8 @@ namespace SW.Bitween.Domain.DataSources; /// every gateway that exists today keeps working untouched. Setting one moves that gateway onto an /// external broker, served by a resident serverless adapter. /// -/// The split matters: this holds the connection — endpoint, credentials, health — and the gateway -/// holds the meaning — which Document, which routes, which filters. One data source can serve many +/// The split matters: this holds the connection - endpoint, credentials, health - and the gateway +/// holds the meaning - which Document, which routes, which filters. One data source can serve many /// gateways, exactly as one RabbitMQ connection serves many queues. /// public class DataSource : BaseEntity, IAudited @@ -31,7 +31,7 @@ public class DataSource : BaseEntity, IAudited /// /// Connection settings handed to the adapter as startup values. Deliberately untyped: a /// provider must not be constrained to the subset of a broker's model that Bitween happens to - /// have modelled. Secrets are protected at rest — see . + /// have modelled. Secrets are protected at rest - see . /// public Dictionary Properties { get; set; } = new(); @@ -43,7 +43,7 @@ public class DataSource : BaseEntity, IAudited /// /// How many nodes may run this source's adapter at once. Left - /// it follows from , which is right almost always — set it only to override. + /// it follows from , which is right almost always - set it only to override. /// public DataSourcePlacement Placement { get; set; } = DataSourcePlacement.Auto; @@ -52,8 +52,8 @@ public class DataSource : BaseEntity, IAudited /// /// How long a message's dedupe key is remembered. It has to exceed the widest redelivery - /// window this broker can actually produce — its message TTL, a dead-letter replay, someone - /// re-driving a queue by hand — because a key forgotten too early lets a redelivery through + /// window this broker can actually produce - its message TTL, a dead-letter replay, someone + /// re-driving a queue by hand - because a key forgotten too early lets a redelivery through /// as a fresh message. That number is a property of the customer's broker, not of Bitween, /// which is why it lives here rather than in configuration. /// @@ -70,7 +70,7 @@ public class DataSource : BaseEntity, IAudited /// /// A hard ceiling in megabytes, enforced by the runtime rather than by the supervisor's - /// goodwill — it becomes the adapter process's GC heap hard limit, so an allocation past it + /// goodwill - it becomes the adapter process's GC heap hard limit, so an allocation past it /// fails inside the adapter instead of taking the node down with it. Zero leaves the host's /// own default in place. /// @@ -81,12 +81,12 @@ public class DataSource : BaseEntity, IAudited public int HardMemoryLimitMb { get; set; } /// - /// Sustained CPU ceiling for the adapter process, as a percentage of the WHOLE node — the same + /// Sustained CPU ceiling for the adapter process, as a percentage of the WHOLE node - the same /// figure the heartbeat reports. Worth being exact about, because the intuitive reading is /// wrong in an expensive direction: one core pegged flat out on a sixteen-core node reads about /// 6%, so a ceiling set at "50%, surely that's half a core" would in fact allow eight. /// - /// Deliberately sustained rather than instantaneous — an adapter draining a backlog is supposed + /// Deliberately sustained rather than instantaneous - an adapter draining a backlog is supposed /// to work hard, and recycling it for that would be a bug wearing a limit's clothes. Zero /// leaves the host default in place. /// @@ -120,7 +120,7 @@ public class DataSource : BaseEntity, IAudited } /// -/// Broker today. The others are declared now because the adapter contract is identical for them — +/// Broker today. The others are declared now because the adapter contract is identical for them - /// a relational or object-store source differs in whether it pushes or is polled, not in how it is /// configured, supervised or observed. /// @@ -137,7 +137,7 @@ public enum DataSourceKind /// How many nodes may hold this connection. /// /// The distinction is not cosmetic and the default is not safe in both directions. A broker queue -/// consumed by two nodes is duplicate processing — the failure the whole leased design exists to +/// consumed by two nodes is duplicate processing - the failure the whole leased design exists to /// prevent. A database connection pool held by only one node is the opposite failure: every other /// node's Xchanges have nowhere to run, and the health page reports "not running here" as though /// that were normal. diff --git a/SW.Bitween.Api/Domain/DataSources/DataSourceStatement.cs b/SW.Bitween.Api/Domain/DataSources/DataSourceStatement.cs index 28908c50..49eac644 100644 --- a/SW.Bitween.Api/Domain/DataSources/DataSourceStatement.cs +++ b/SW.Bitween.Api/Domain/DataSources/DataSourceStatement.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.Domain.DataSources; /// One named piece of SQL a data source is allowed to run. /// /// This is deliberately an entity rather than a JSON blob on , and the -/// reason is permissions before anything else. Statements have to live on the connection — the +/// reason is permissions before anything else. Statements have to live on the connection - the /// alternative, SQL in a subscription's adapter properties, is a live injection surface, because /// those property values have {{partner.X}} substituted into them before the adapter ever /// sees them, and partner records are ordinary data. But putting the SQL in a field on the data @@ -20,8 +20,8 @@ namespace SW.Bitween.Domain.DataSources; /// one field, where a bad edit fails the connection test for everyone. /// * Namespacing. A unique index on (data source, name) makes a collision an error at the /// point of saving rather than a silent overwrite. -/// * Dead SQL. A statement nothing references is now a countable fact — see the usage -/// endpoint — instead of a line nobody dares delete. +/// * Dead SQL. A statement nothing references is now a countable fact - see the usage +/// endpoint - instead of a line nobody dares delete. /// /// What does NOT change is the adapter contract. The supervisor composes these rows into the same /// Statements JSON the adapter has always received, so an adapter still resolves a name and @@ -33,7 +33,7 @@ public class DataSourceStatement : BaseEntity, IAudited public DataSource DataSource { get; set; } /// - /// What a subscription names to run it. Unique within the data source, case-insensitively — + /// What a subscription names to run it. Unique within the data source, case-insensitively - /// the adapter resolves names that way, so allowing getOrder and GetOrder to /// coexist would make which one runs a matter of dictionary ordering. /// @@ -56,7 +56,7 @@ public class DataSourceStatement : BaseEntity, IAudited public int? WorkGroupId { get; set; } /// - /// Kept out of the composed statement set without being deleted — the same idea as + /// Kept out of the composed statement set without being deleted - the same idea as /// . Useful for retiring a statement while the subscriptions /// that used it are still being migrated: they fail loudly on a missing name rather than /// quietly running SQL nobody meant to keep. @@ -64,7 +64,7 @@ public class DataSourceStatement : BaseEntity, IAudited public bool Inactive { get; set; } /// - /// For a statement a receiver polls with: the column carrying the cursor — the incrementing + /// For a statement a receiver polls with: the column carrying the cursor - the incrementing /// id, or the modified-at timestamp. Its value in the last row read is what gets saved. /// /// It lives here rather than on the subscription because it describes the SHAPE of what this @@ -78,7 +78,7 @@ public class DataSourceStatement : BaseEntity, IAudited /// /// For a polled statement: the column identifying a row, used for mark-processed and for - /// deduplication. Here for the same reason as — it is a fact about + /// deduplication. Here for the same reason as - it is a fact about /// the query's result, not about who reads it. /// public string KeyColumn { get; set; } diff --git a/SW.Bitween.Api/Domain/DataSources/InboundMessage.cs b/SW.Bitween.Api/Domain/DataSources/InboundMessage.cs index 39dc153d..af5fb506 100644 --- a/SW.Bitween.Api/Domain/DataSources/InboundMessage.cs +++ b/SW.Bitween.Api/Domain/DataSources/InboundMessage.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.Domain.DataSources; /// /// One inbound message we have already persisted, remembered by its dedupe key. /// -/// At-least-once delivery is not an edge case here — it is what persist-then-acknowledge buys. +/// At-least-once delivery is not an edge case here - it is what persist-then-acknowledge buys. /// A crash between committing the Xchange and acknowledging the broker redelivers by design, so /// duplicates are normal and something has to recognise them. /// diff --git a/SW.Bitween.Api/Domain/DelayedRetry.cs b/SW.Bitween.Api/Domain/DelayedRetry.cs index c8bd1ffc..12ff4e19 100644 --- a/SW.Bitween.Api/Domain/DelayedRetry.cs +++ b/SW.Bitween.Api/Domain/DelayedRetry.cs @@ -6,4 +6,10 @@ namespace SW.Bitween.Domain; public class DelayedRetry : BaseEntity { public DateTime On { get; set; } + + /// + /// How many times running this retry has failed. A failure is rescheduled, not dropped - a + /// database blip used to delete the retry for good - up to . + /// + public int RunFailures { get; set; } } diff --git a/SW.Bitween.Api/Domain/Document/Document.cs b/SW.Bitween.Api/Domain/Document/Document.cs index ce7e595d..ceb7a0aa 100644 --- a/SW.Bitween.Api/Domain/Document/Document.cs +++ b/SW.Bitween.Api/Domain/Document/Document.cs @@ -24,7 +24,7 @@ public Document(int id, string name, DocumentFormat format) DocumentFormat = format; } - /// Production creation path — Id is database-generated. Code is optional. + /// Production creation path - Id is database-generated. Code is optional. public Document(string code, string name, DocumentFormat format) { Code = code; @@ -69,6 +69,18 @@ public void SetCode(string code) Code = code; } + /// + /// JSON Schema for a JSON type, XSD for an XML one; null checks nothing. What arrives at a + /// gateway that doesn't match is refused, and an exchange whose input doesn't match fails. + /// A private setter so a save that doesn't mention it leaves it alone. + /// + public string ValidationSchema { get; private set; } + + public void SetValidationSchema(string schema) + { + ValidationSchema = string.IsNullOrWhiteSpace(schema) ? null : schema; + } + public void Retire() { RetiredOn = DateTime.UtcNow; diff --git a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs index da311309..8973be05 100644 --- a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs @@ -12,7 +12,7 @@ public class ApiGateway : BaseEntity,IAudited /// /// Turns the gateway off without deleting it. Deleting is the only alternative today, - /// and it takes the partner attachments with it — so a gateway that needs stopping for + /// and it takes the partner attachments with it - so a gateway that needs stopping for /// an afternoon gets rebuilt by hand afterwards, or left running. /// public bool Inactive { get; set; } @@ -34,6 +34,12 @@ public class ApiGateway : BaseEntity,IAudited public string JwtAudience { get; set; } public string JwtPartnerClaim { get; set; } + /// + /// What a sync call is told when the delivery behind it fails. Legacy (400, no body) for gateways + /// made before this existed: a caller may be written against it. + /// + public GatewayFailureAnswer FailureAnswer { get; set; } + public ICollection Partners { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs index 31909816..2691d333 100644 --- a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs @@ -10,13 +10,13 @@ public class BusGateway : BaseEntity, IAudited public int DocumentId { get; set; } /// - /// Turns the gateway off without deleting it — its routes stop being offered the + /// Turns the gateway off without deleting it - its routes stop being offered the /// message. See . /// public bool Inactive { get; set; } /// - /// Null means the INTERNAL bus — the only behaviour that existed before, and still the + /// Null means the INTERNAL bus - the only behaviour that existed before, and still the /// default, so every gateway already in a database keeps working with no migration of data. /// Set it and this gateway is fed by an external broker through a resident adapter instead. /// @@ -30,14 +30,14 @@ public class BusGateway : BaseEntity, IAudited public string Endpoint { get; set; } /// - /// Per-gateway overrides handed to the adapter alongside the data source's own properties — + /// Per-gateway overrides handed to the adapter alongside the data source's own properties - /// prefetch, consumer group, visibility timeout. Connection settings belong on the DataSource; /// these are about this one subscription to it. /// /// NOT YET CONSUMED. The supervisor namespaces these and passes them through as /// Endpoint:<endpoint>:<key> startup values, but neither bundled adapter reads /// them, so setting one currently has no effect. The transport is in place; binding them is - /// per-adapter work — RabbitMQ can apply queue type and durability per endpoint at declare + /// per-adapter work - RabbitMQ can apply queue type and durability per endpoint at declare /// time, SQS a per-queue visibility timeout, and prefetch would need a consume channel per /// endpoint rather than the single one it has now. /// diff --git a/SW.Bitween.Api/Domain/GlobalAdapterValue/GlobalAdapterValuesSet.cs b/SW.Bitween.Api/Domain/GlobalAdapterValue/GlobalAdapterValuesSet.cs index cb686ff2..326c7618 100644 --- a/SW.Bitween.Api/Domain/GlobalAdapterValue/GlobalAdapterValuesSet.cs +++ b/SW.Bitween.Api/Domain/GlobalAdapterValue/GlobalAdapterValuesSet.cs @@ -12,7 +12,7 @@ public class GlobalAdapterValuesSet:BaseEntity /// /// Names within whose values are never returned by the API in clear. /// A set is shared across every adapter that references it, so one secret in a set must not - /// force the whole set out of sight — the flag is per value, not per set. + /// force the whole set out of sight - the flag is per value, not per set. /// public List SecretProperties { get; set; } = new(); } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/IdempotencyKey.cs b/SW.Bitween.Api/Domain/IdempotencyKey.cs new file mode 100644 index 00000000..b240e088 --- /dev/null +++ b/SW.Bitween.Api/Domain/IdempotencyKey.cs @@ -0,0 +1,32 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// A partner's Idempotency-Key, and the exchange the first call carrying it created. A partner +/// whose call timed out can't tell whether it arrived; sending it again with the same key returns +/// that exchange instead of making a second one. +/// +public class IdempotencyKey +{ + private IdempotencyKey() + { + } + + public IdempotencyKey(string scope, string key, string xchangeId) + { + Scope = scope; + Key = key; + XchangeId = xchangeId; + CreatedOn = DateTime.UtcNow; + } + + public long Id { get; private set; } + + /// Who the key belongs to - gateway and partner - so two callers can't collide. + public string Scope { get; private set; } + + public string Key { get; private set; } + public string XchangeId { get; private set; } + public DateTime CreatedOn { get; private set; } +} diff --git a/SW.Bitween.Api/Domain/Metrics/AdapterStats.cs b/SW.Bitween.Api/Domain/Metrics/AdapterStats.cs new file mode 100644 index 00000000..656c220e --- /dev/null +++ b/SW.Bitween.Api/Domain/Metrics/AdapterStats.cs @@ -0,0 +1,40 @@ +using System; + +namespace SW.Bitween.Domain.Metrics; + +/// +/// One hour of calls to one version of one adapter, in one role, on one runtime, as one node made +/// them. Counted where every adapter call goes through (AdapterInvoker), so receivers, validators, +/// mappers and handlers, native or not, are all here. +/// +public class AdapterStats : TimedStats +{ + public DateTime Hour { get; set; } + public string Node { get; set; } + public string AdapterId { get; set; } + /// The version that ran; empty for a built-in adapter or one published before catalogs. + public string Version { get; set; } + /// receiver, validator, mapper, handler. + public string Role { get; set; } + /// native (in-process), resident (pooled process), process (one process per call). + public string Runtime { get; set; } + + public int Calls { get; set; } + /// Every call that failed, whatever the cause; the three below are some of them. + public int Failures { get; set; } + public int Timeouts { get; set; } + /// Stopped by the runtime for going over a memory or CPU limit. + public int LimitStops { get; set; } + /// The adapter's process died or stopped answering. + public int Crashes { get; set; } + + public void Add(AdapterStats other) + { + Calls += other.Calls; + Failures += other.Failures; + Timeouts += other.Timeouts; + LimitStops += other.LimitStops; + Crashes += other.Crashes; + AddTimes(other); + } +} diff --git a/SW.Bitween.Api/Domain/Metrics/DurationBuckets.cs b/SW.Bitween.Api/Domain/Metrics/DurationBuckets.cs new file mode 100644 index 00000000..4dc23296 --- /dev/null +++ b/SW.Bitween.Api/Domain/Metrics/DurationBuckets.cs @@ -0,0 +1,89 @@ +using System; + +namespace SW.Bitween.Domain.Metrics; + +/// +/// Durations counted into ten fixed buckets rather than kept one by one, so an hour of a million +/// exchanges is still ten numbers, buckets from many hours and nodes add up, and a percentile is +/// read off the counts. The answer is the bucket's upper edge: "p95 under 2.5 s", which is as exact +/// as a dashboard needs. +/// +public static class DurationBuckets +{ + /// Upper edges in milliseconds; the last bucket has no edge. + public static readonly int[] UpperMs = [50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000]; + + public const int Count = 10; + + public static int IndexOf(long ms) + { + for (var i = 0; i < UpperMs.Length; i++) + if (ms < UpperMs[i]) return i; + return Count - 1; + } + + /// + /// The duration under which of the counted ones fell (0.95 for p95), + /// or null when nothing was counted. Past the last edge, the longest one seen. + /// + public static long? Percentile(long[] buckets, double fraction, long maxMs) + { + long total = 0; + foreach (var b in buckets) total += b; + if (total == 0) return null; + var target = (long)Math.Ceiling(total * fraction); + long seen = 0; + for (var i = 0; i < buckets.Length; i++) + { + seen += buckets[i]; + if (seen >= target) return i < UpperMs.Length ? Math.Min(UpperMs[i], Math.Max(maxMs, 1)) : maxMs; + } + return maxMs; + } +} + +/// The ten bucket counts, as columns, on every rollup that times something. +public abstract class TimedStats +{ + public long DurationSumMs { get; set; } + public long DurationMaxMs { get; set; } + public int D0 { get; set; } + public int D1 { get; set; } + public int D2 { get; set; } + public int D3 { get; set; } + public int D4 { get; set; } + public int D5 { get; set; } + public int D6 { get; set; } + public int D7 { get; set; } + public int D8 { get; set; } + public int D9 { get; set; } + + public long[] Buckets() => [D0, D1, D2, D3, D4, D5, D6, D7, D8, D9]; + + public void AddDuration(long ms, int times = 1) + { + DurationSumMs += ms * times; + if (ms > DurationMaxMs) DurationMaxMs = ms; + switch (DurationBuckets.IndexOf(ms)) + { + case 0: D0 += times; break; + case 1: D1 += times; break; + case 2: D2 += times; break; + case 3: D3 += times; break; + case 4: D4 += times; break; + case 5: D5 += times; break; + case 6: D6 += times; break; + case 7: D7 += times; break; + case 8: D8 += times; break; + default: D9 += times; break; + } + } + + public void AddTimes(TimedStats other) + { + DurationSumMs += other.DurationSumMs; + if (other.DurationMaxMs > DurationMaxMs) DurationMaxMs = other.DurationMaxMs; + D0 += other.D0; D1 += other.D1; D2 += other.D2; D3 += other.D3; D4 += other.D4; + D5 += other.D5; D6 += other.D6; D7 += other.D7; D8 += other.D8; D9 += other.D9; + } +} diff --git a/SW.Bitween.Api/Domain/Metrics/ErrorStats.cs b/SW.Bitween.Api/Domain/Metrics/ErrorStats.cs new file mode 100644 index 00000000..be6e4698 --- /dev/null +++ b/SW.Bitween.Api/Domain/Metrics/ErrorStats.cs @@ -0,0 +1,26 @@ +using System; + +namespace SW.Bitween.Domain.Metrics; + +/// +/// How often one kind of error happened in an hour, for an adapter, a subscription or a gateway. +/// Errors are told apart by a fingerprint of their message with the parts that change every time +/// (ids, numbers, dates) taken out, so "Order 1043 not found" and "Order 2210 not found" count as +/// one, with the latest whole message kept as an example. +/// +public class ErrorStats +{ + public const string AdapterScope = "adapter"; + public const string SubscriptionScope = "subscription"; + public const string GatewayScope = "gateway"; + + public DateTime Hour { get; set; } + public string Node { get; set; } + public string Scope { get; set; } + /// The adapter id, subscription id or gateway url name. + public string ScopeKey { get; set; } + public string Fingerprint { get; set; } + public string Sample { get; set; } + public int Count { get; set; } + public DateTime LastSeenOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/Metrics/ExchangeStats.cs b/SW.Bitween.Api/Domain/Metrics/ExchangeStats.cs new file mode 100644 index 00000000..55ca656f --- /dev/null +++ b/SW.Bitween.Api/Domain/Metrics/ExchangeStats.cs @@ -0,0 +1,56 @@ +using System; + +namespace SW.Bitween.Domain.Metrics; + +/// +/// One hour of exchanges for one subscription and partner, as one node processed them. Counted in +/// memory as each exchange finishes and added in here once a minute (see MetricsFlusher), so no +/// page ever counts exchanges themselves. Hours are those the exchanges started in. +/// +/// +/// A node's rows carry its name, so nodes never write the same row. History from before metrics +/// were recorded is folded in once under . +/// +public class ExchangeStats : TimedStats +{ + /// The node name history rows are written under. + public const string HistoryNode = "(history)"; + + /// 0 when there is none: an exchange routed by information type alone. + public const int None = 0; + + /// The subscription and partner of the row that marks an hour of history as done. + public const int HourDone = -1; + + public DateTime Hour { get; set; } + public string Node { get; set; } + public int SubscriptionId { get; set; } + public int PartnerId { get; set; } + + public int Total { get; set; } + public int Succeeded { get; set; } + public int BadResponse { get; set; } + public int Failed { get; set; } + public int FailedAtMapper { get; set; } + public int FailedAtHandler { get; set; } + /// Exchanges that were themselves retries of another. + public int Retries { get; set; } + public int ManualRetries { get; set; } + public long BytesIn { get; set; } + public long BytesOut { get; set; } + + public void Add(ExchangeStats other) + { + Total += other.Total; + Succeeded += other.Succeeded; + BadResponse += other.BadResponse; + Failed += other.Failed; + FailedAtMapper += other.FailedAtMapper; + FailedAtHandler += other.FailedAtHandler; + Retries += other.Retries; + ManualRetries += other.ManualRetries; + BytesIn += other.BytesIn; + BytesOut += other.BytesOut; + AddTimes(other); + } +} diff --git a/SW.Bitween.Api/Domain/Metrics/QueueSnapshot.cs b/SW.Bitween.Api/Domain/Metrics/QueueSnapshot.cs new file mode 100644 index 00000000..10806d2f --- /dev/null +++ b/SW.Bitween.Api/Domain/Metrics/QueueSnapshot.cs @@ -0,0 +1,20 @@ +using System; + +namespace SW.Bitween.Domain.Metrics; + +/// +/// The broker's totals at one moment, taken every few minutes, so the backlog can be shown as a +/// trend and not only as it is right now. Kept for a month. +/// +public class QueueSnapshot +{ + /// The time, cut to the sampling interval: two nodes sampling together keep one. + public DateTime At { get; set; } + public long QueueDepth { get; set; } + public long RetryBacklog { get; set; } + public long DeadLetterBacklog { get; set; } + public double IncomingRate { get; set; } + public double AckRate { get; set; } + public int Consumers { get; set; } + public int UnhealthyConsumers { get; set; } +} diff --git a/SW.Bitween.Api/Domain/OperatorAction.cs b/SW.Bitween.Api/Domain/OperatorAction.cs new file mode 100644 index 00000000..2bc0c352 --- /dev/null +++ b/SW.Bitween.Api/Domain/OperatorAction.cs @@ -0,0 +1,45 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// Something a member did that changes what Bitween does without changing its configuration: +/// retrying exchanges, exporting them, requeueing dead letters, deleting queues, running a scheduled +/// retry now. Recorded so the audit trail answers "who did that", as it does for configuration. +/// +public class OperatorAction +{ + public const string Retry = "retry"; + public const string BulkRetry = "bulk-retry"; + public const string Export = "export"; + public const string RunRetryNow = "run-retry-now"; + public const string RequeueDeadLetters = "requeue-dead-letters"; + public const string DeleteQueues = "delete-queues"; + + private OperatorAction() + { + } + + public OperatorAction(string action, string target, string detail, string accountId) + { + Action = action; + Target = Clip(target, 500); + Detail = Clip(detail, 2000); + AccountId = accountId; + OccurredOn = DateTime.UtcNow; + } + + public long Id { get; private set; } + public string Action { get; private set; } + + /// What it was done to: an exchange id, a queue name, a selection. + public string Target { get; private set; } + + /// How much it touched, in a sentence: "12 exchanges", "3 queues deleted, 1 skipped". + public string Detail { get; private set; } + + public string AccountId { get; private set; } + public DateTime OccurredOn { get; private set; } + + static string Clip(string value, int max) => value == null || value.Length <= max ? value : value[..max]; +} diff --git a/SW.Bitween.Api/Domain/OutboxMessage.cs b/SW.Bitween.Api/Domain/OutboxMessage.cs new file mode 100644 index 00000000..52490f13 --- /dev/null +++ b/SW.Bitween.Api/Domain/OutboxMessage.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace SW.Bitween.Domain; + +/// +/// A bus message waiting to be published, written in the same transaction as the change that +/// raised it. +/// +/// +/// Messages used to be published straight after the commit. When the broker was unreachable at +/// that moment, the change was saved and its message was lost: an exchange that was stored and +/// never processed, with nothing to notice. Now the message is stored with the change, published +/// straight after the commit as before, and - when that fails - published later by +/// OutboxDispatcher. Delivery is at least once, so consumers must tolerate a repeat. +/// +public class OutboxMessage +{ + private OutboxMessage() + { + } + + public OutboxMessage(string messageType, string body, Dictionary headers = null) + { + MessageType = messageType; + Body = body; + Headers = headers; + CreatedOn = DateTime.UtcNow; + } + + public long Id { get; private set; } + public string MessageType { get; private set; } + public string Body { get; private set; } + public Dictionary Headers { get; private set; } + public DateTime CreatedOn { get; private set; } + + /// Null until the broker has taken it. + public DateTime? PublishedOn { get; private set; } + + /// A dispatcher's claim on it, so two replicas never publish the same row at once. + public DateTime? ClaimedUntil { get; private set; } + + public int Attempts { get; private set; } + public string LastError { get; private set; } +} diff --git a/SW.Bitween.Api/Domain/Partner/ApiCredential.cs b/SW.Bitween.Api/Domain/Partner/ApiCredential.cs index da21c7e7..57de7900 100644 --- a/SW.Bitween.Api/Domain/Partner/ApiCredential.cs +++ b/SW.Bitween.Api/Domain/Partner/ApiCredential.cs @@ -8,15 +8,23 @@ private ApiCredential() { } + /// The key itself; only its hash is kept. See . public ApiCredential(string name, string key) { Name = name ?? throw new ArgumentNullException(nameof(name)); - Key = key ?? throw new ArgumentNullException(nameof(key)); + if (key == null) throw new ArgumentNullException(nameof(key)); + Key = PartnerKeyHash.Stored(key); + if (!PartnerKeyHash.IsHashed(key)) KeyPrefix = key.Length > 5 ? key[..5] : key; } public string Name { get; private set; } + + /// The key's hash - sha256:... - or, on a row not yet converted, the key. public string Key { get; private set; } + /// The first characters of the key, so a screen can tell one key from another. + public string KeyPrefix { get; private set; } + public override bool Equals(object obj) { return obj is ApiCredential credential && diff --git a/SW.Bitween.Api/Domain/Partner/ApiKeyUse.cs b/SW.Bitween.Api/Domain/Partner/ApiKeyUse.cs new file mode 100644 index 00000000..85c1ee22 --- /dev/null +++ b/SW.Bitween.Api/Domain/Partner/ApiKeyUse.cs @@ -0,0 +1,26 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// When a partner's API key last got a call through a gateway. Kept beside the key rather than on +/// it, so a gateway call writes one small row instead of the partner; written at most every few +/// minutes per key and node (see ), so it is approximate by that much. +/// +public class ApiKeyUse +{ + private ApiKeyUse() + { + } + + public ApiKeyUse(int partnerId, string keyName, DateTime usedOn) + { + PartnerId = partnerId; + KeyName = keyName; + LastUsedOn = usedOn; + } + + public int PartnerId { get; private set; } + public string KeyName { get; private set; } + public DateTime LastUsedOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/Partner/Partner.cs b/SW.Bitween.Api/Domain/Partner/Partner.cs index 33332e54..c0d0975d 100644 --- a/SW.Bitween.Api/Domain/Partner/Partner.cs +++ b/SW.Bitween.Api/Domain/Partner/Partner.cs @@ -36,7 +36,7 @@ public Partner(string name) /// Names within whose values are never returned by the API /// in clear. Opt-in and freely reversible: a partner property is ordinary text until /// someone marks it, which is why the list lives here rather than being inferred from the - /// key's name — inferring would have hidden values that were readable yesterday. + /// key's name - inferring would have hidden values that were readable yesterday. /// public List SecretProperties { get; set; } = new(); diff --git a/SW.Bitween.Api/Domain/Partner/PartnerKeyHash.cs b/SW.Bitween.Api/Domain/Partner/PartnerKeyHash.cs new file mode 100644 index 00000000..f6fdb033 --- /dev/null +++ b/SW.Bitween.Api/Domain/Partner/PartnerKeyHash.cs @@ -0,0 +1,28 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SW.Bitween.Domain +{ + /// + /// Partner API keys are stored as a SHA-256 of the key, never the key itself, so a copy of the + /// database no longer lets anyone call Bitween as every partner. + /// + /// + /// Unsalted on purpose. Keys are random - 128 bits from a GUID - so there is nothing to look up + /// in a table, and an unsalted hash keeps the lookup a single indexed equality. + /// + public static class PartnerKeyHash + { + public const string Prefix = "sha256:"; + + public static string Of(string key) => + Prefix + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))).ToLowerInvariant(); + + public static bool IsHashed(string stored) => + stored != null && stored.StartsWith(Prefix, StringComparison.Ordinal); + + /// The stored form of a key, whichever form it arrives in. + public static string Stored(string key) => IsHashed(key) ? key : Of(key); + } +} diff --git a/SW.Bitween.Api/Domain/ReceiveAttempt.cs b/SW.Bitween.Api/Domain/ReceiveAttempt.cs index a7ceaa54..e887a211 100644 --- a/SW.Bitween.Api/Domain/ReceiveAttempt.cs +++ b/SW.Bitween.Api/Domain/ReceiveAttempt.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.Domain; /// /// One execution of a Receiving subscription's receive step, written by ReceivingJob -/// itself right where it already catches the receiver's own failures — kept independent of +/// itself right where it already catches the receiver's own failures - kept independent of /// Quartz's own run history and unaffected by how Quartz treats a thrown exception. /// public class ReceiveAttempt : BaseEntity diff --git a/SW.Bitween.Api/Domain/RetryAlertOverride.cs b/SW.Bitween.Api/Domain/RetryAlertOverride.cs index 61d1b6b9..d8c71cfc 100644 --- a/SW.Bitween.Api/Domain/RetryAlertOverride.cs +++ b/SW.Bitween.Api/Domain/RetryAlertOverride.cs @@ -30,6 +30,6 @@ public class RetryAlertOverride /// Adapter that delivers the alert. Required when is Send. public string AlertHandlerId { get; set; } - /// That adapter's own settings — api key, recipients, subject. + /// That adapter's own settings - api key, recipients, subject. public IReadOnlyDictionary AlertHandlerProperties { get; set; } } diff --git a/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs index b5f7e29f..f9c72524 100644 --- a/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs +++ b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs @@ -33,7 +33,7 @@ public class RetryBudgetExhaustedEvent : BaseDomainEvent /// /// The ceiling that was reached. Not paired with an "used" count, because at exhaustion the two - /// are the same number — except when the ceiling was lowered below what had already been spent, + /// are the same number - except when the ceiling was lowered below what had already been spent, /// where the ceiling is still the meaningful figure. /// public int MaxAttemptsTotal { get; set; } diff --git a/SW.Bitween.Api/Domain/RetryGroupUsage.cs b/SW.Bitween.Api/Domain/RetryGroupUsage.cs index b19f7a9b..fddb123c 100644 --- a/SW.Bitween.Api/Domain/RetryGroupUsage.cs +++ b/SW.Bitween.Api/Domain/RetryGroupUsage.cs @@ -10,8 +10,8 @@ namespace SW.Bitween.Domain; /// /// Once reaches the group's MaxAttemptsTotal the group stops /// retrying for that integration until this row is cleared. A row that has reached the cap is cleared -/// by the integration's next success — the only signal that the downstream it was failing against has -/// recovered — or by one of the reset endpoints. A row still below the cap is left alone by a success: +/// by the integration's next success - the only signal that the downstream it was failing against has +/// recovered - or by one of the reset endpoints. A row still below the cap is left alone by a success: /// the cap is there for a downstream that fails some messages and succeeds others, which is exactly /// when crediting it back would stop it ever being reached. /// @@ -25,7 +25,7 @@ public class RetryGroupUsage public int AttemptsUsed { get; set; } - /// When the last attempt was claimed — the only clue left once a group is exhausted. + /// When the last attempt was claimed - the only clue left once a group is exhausted. public DateTime LastAttemptOn { get; set; } /// diff --git a/SW.Bitween.Api/Domain/RetryPolicy.cs b/SW.Bitween.Api/Domain/RetryPolicy.cs index c452e6f3..e18b0879 100644 --- a/SW.Bitween.Api/Domain/RetryPolicy.cs +++ b/SW.Bitween.Api/Domain/RetryPolicy.cs @@ -16,7 +16,7 @@ public class RetryPolicy : BaseEntity, IAudited, IRetryPolicy /// public string AlertHandlerId { get; set; } - /// That adapter's own settings — api key, recipients, subject. + /// That adapter's own settings - api key, recipients, subject. public IReadOnlyDictionary AlertHandlerProperties { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/Setting.cs b/SW.Bitween.Api/Domain/Setting.cs index 61d566e8..d86e5d91 100644 --- a/SW.Bitween.Api/Domain/Setting.cs +++ b/SW.Bitween.Api/Domain/Setting.cs @@ -5,14 +5,14 @@ namespace SW.Bitween.Domain; /// /// One instance-wide setting, keyed by the setting's catalog key (e.g. Theme.PrimaryColor). -/// This table is the single source of truth: configuration seeds a key once — on the first boot -/// after that key exists — and is ignored for it from then on. Every catalog key normally has a +/// This table is the single source of truth: configuration seeds a key once - on the first boot +/// after that key exists - and is ignored for it from then on. Every catalog key normally has a /// row; "reset to default" rewrites the row with the product default rather than removing it. /// /// Deliberately a plain key/value store: the definition of a key (label, section, type, /// whether it's a secret) lives in , so adding a /// setting never needs a migration or a data fix-up. A secret's value is encrypted before it -/// gets here — see . +/// gets here - see . /// /// public class Setting : BaseEntity, IAudited diff --git a/SW.Bitween.Api/Domain/Subscription/Schedule.cs b/SW.Bitween.Api/Domain/Subscription/Schedule.cs index eaed7744..3a4b06c4 100644 --- a/SW.Bitween.Api/Domain/Subscription/Schedule.cs +++ b/SW.Bitween.Api/Domain/Subscription/Schedule.cs @@ -57,7 +57,7 @@ public override int GetHashCode() public DateTime Next(DateTime? currentDate = null) { - // On.Hours/On.Minutes are UTC wall-clock time — the Quartz cron trigger that + // On.Hours/On.Minutes are UTC wall-clock time - the Quartz cron trigger that // actually fires this schedule is explicitly pinned to TimeZoneInfo.Utc // (see SW.Scheduler's WithCronSchedule(...).InTimeZone(TimeZoneInfo.Utc) call // sites), so this must agree rather than each side defaulting independently. diff --git a/SW.Bitween.Api/Domain/Subscription/Subscription.cs b/SW.Bitween.Api/Domain/Subscription/Subscription.cs index 7647247d..f2457180 100644 --- a/SW.Bitween.Api/Domain/Subscription/Subscription.cs +++ b/SW.Bitween.Api/Domain/Subscription/Subscription.cs @@ -75,8 +75,34 @@ private Subscription(string name, int documentId, SubscriptionType type, int? pa public WorkGroup WorkGroup { get; set; } public bool Temporary { get; private set; } public DateTime? PausedOn { get; private set; } + /// - /// Which external system this subscription's adapters connect through — a database, typically. + /// Failed deliveries in a row that pause the subscription; null never does. Counted in + /// , which receivers and aggregations use for their runs. + /// + public int? AutoPauseAfterFailures { get; set; } + + /// Paused by rather than by a person. + public bool PausedAutomatically { get; private set; } + + /// How often an exchange should arrive; null expects nothing. Validated by . + public ExpectTraffic ExpectTraffic { get; private set; } + + /// + /// When QuietSubscriptionsJob found no exchange where wanted one; + /// null while traffic arrives. Written by the job alone, with a conditional update, so two nodes + /// can't both raise it. + /// + public DateTime? QuietSince { get; private set; } + + public void SetExpectTraffic(ExpectTraffic expectTraffic) + { + ExpectTraffic = expectTraffic; + // Nothing is expected any more, so nothing can be missing; left set it would show for good. + if (expectTraffic == null) QuietSince = null; + } + /// + /// Which external system this subscription's adapters connect through - a database, typically. /// /// Null keeps every existing subscription exactly as it was: an adapter carries its own /// connection settings in its properties. Setting one moves that job to the data source, which @@ -94,6 +120,21 @@ private Subscription(string name, int documentId, SubscriptionType type, int? pa public string ReceiverId { get; set; } public string MapperId { get; set; } + + /// + /// Published versions this subscription is pinned to, one per adapter slot; null follows + /// whatever version is current - which is how every subscription behaved before versions. + /// + public string ReceiverVersion { get; set; } + public string ValidatorVersion { get; set; } + public string MapperVersion { get; set; } + public string HandlerVersion { get; set; } + + /// What actually runs for a slot: the adapter id, or {id}/{version} when pinned. + public string ReceiverRef => SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(ReceiverId, ReceiverVersion); + public string ValidatorRef => SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(ValidatorId, ValidatorVersion); + public string MapperRef => SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(MapperId, MapperVersion); + public string HandlerRef => SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(HandlerId, HandlerVersion); public IReadOnlyDictionary ValidatorProperties { get; private set; } public IReadOnlyDictionary HandlerProperties { get; private set; } public IReadOnlyDictionary MapperProperties { get; private set; } @@ -102,6 +143,19 @@ private Subscription(string name, int documentId, SubscriptionType type, int? pa public IPropertyMatchSpecification MatchExpression { get; private set; } public bool IsRunning { get; set; } + + /// + /// When the run that set started. A process killed mid-run never clears + /// the flag, so without a time the subscription stayed "running" - and never ran - for good. + /// + public DateTime? RunningSince { get; private set; } + + /// Lets the next scheduled run start: for a flag left behind by a run that died. + public void ClearRunning() + { + IsRunning = false; + RunningSince = null; + } public bool Inactive { get; set; } public int? ResponseSubscriptionId { get; set; } /// @@ -183,7 +237,7 @@ public void SetMatchExpression(IPropertyMatchSpecification matchExpression) /// /// Assigns the subscription's retry policy. An inline - /// always takes precedence over a referenced — the two are + /// always takes precedence over a referenced - the two are /// mutually exclusive, matching the resolution order used at evaluation time /// (CustomRetryPolicy ?? RetryPolicy). Setting a custom policy clears any referenced /// one and vice versa, so the two fields can never disagree. @@ -205,11 +259,21 @@ public void SetRetryPolicy(int? retryPolicyId, CustomRetryPolicy customRetryPoli public void Pause() { PausedOn = DateTime.UtcNow; + PausedAutomatically = false; } public void UnPause() { + // Resuming after an automatic pause starts the count again; otherwise the next failure + // would pause it straight back. + if (PausedAutomatically) + { + ConsecutiveFailures = 0; + LastException = null; + } + PausedOn = null; + PausedAutomatically = false; Events.Add(new SubscriptionUnpausedEvent { Id = Id diff --git a/SW.Bitween.Api/Domain/Subscription/SubscriptionAutoPausedEvent.cs b/SW.Bitween.Api/Domain/Subscription/SubscriptionAutoPausedEvent.cs new file mode 100644 index 00000000..22aed1cc --- /dev/null +++ b/SW.Bitween.Api/Domain/Subscription/SubscriptionAutoPausedEvent.cs @@ -0,0 +1,20 @@ +using System; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// A subscription paused itself after failing deliveries in a row. Picked up +/// by its own consumer, as is, so a broken alert handler +/// holds up nothing else. +/// +public class SubscriptionAutoPausedEvent : BaseDomainEvent +{ + public int SubscriptionId { get; set; } + + /// The failure that paused it. + public string XchangeId { get; set; } + + public int Failures { get; set; } + public DateTime OccurredOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/Xchange/OnHoldXchange.cs b/SW.Bitween.Api/Domain/Xchange/OnHoldXchange.cs index f62ee190..505fb8f3 100644 --- a/SW.Bitween.Api/Domain/Xchange/OnHoldXchange.cs +++ b/SW.Bitween.Api/Domain/Xchange/OnHoldXchange.cs @@ -29,7 +29,7 @@ public OnHoldXchange(Subscription subscription, string data, string fileName = n public string[] References { get; private set; } /// - /// The partner to run as once released, when it is not the subscription's own — a response + /// The partner to run as once released, when it is not the subscription's own - a response /// runs as the partner of the subscription that fed it. Null means the subscription's own. /// public int? PartnerId { get; private set; } diff --git a/SW.Bitween.Api/Domain/Xchange/Xchange.cs b/SW.Bitween.Api/Domain/Xchange/Xchange.cs index 658a24a7..ff8a100e 100644 --- a/SW.Bitween.Api/Domain/Xchange/Xchange.cs +++ b/SW.Bitween.Api/Domain/Xchange/Xchange.cs @@ -54,8 +54,9 @@ public Xchange(Subscription subscription, XchangeFile file, string[] references this(subscription.DocumentId, subscription.WorkGroup, file, references, subscription.Type) { SubscriptionId = subscription.Id; - MapperId = subscription.MapperId; - HandlerId = subscription.HandlerId; + // The pinned version travels with the exchange, so a retry runs the same package. + MapperId = subscription.MapperRef; + HandlerId = subscription.HandlerRef; ResponseSubscriptionId = subscription.ResponseSubscriptionId; ResponseMessageTypeName = subscription.ResponseMessageTypeName; PartnerId = gatewayPartner?.Id ?? subscription.PartnerId; @@ -96,8 +97,9 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Par ManualRetry = manualRetry; SubscriptionId = xchange.SubscriptionId; PartnerId = xchange.PartnerId ?? subscription.PartnerId; - MapperId = subscription.MapperId; - HandlerId = subscription.HandlerId; + // The pinned version travels with the exchange, so a retry runs the same package. + MapperId = subscription.MapperRef; + HandlerId = subscription.HandlerRef; MapperProperties = (subscription.MapperProperties ?? new Dictionary()).ToDictionary() .Fill(gatewayPartner, globalAdapterValuesSets).WithDataSource(subscription.DataSourceId); HandlerProperties = (subscription.HandlerProperties ?? new Dictionary()).ToDictionary() @@ -134,8 +136,8 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Par public string SourceXchangeId { get; private set; } /// - /// What the subscription reads from the input of — the original - /// document — by path, as it was when this exchange was created. A path the document had no + /// What the subscription reads from the input of - the original + /// document - by path, as it was when this exchange was created. A path the document had no /// value at is left out. /// /// diff --git a/SW.Bitween.Api/Domain/Xchange/XchangeSource.cs b/SW.Bitween.Api/Domain/Xchange/XchangeSource.cs index 644f52e8..e8045884 100644 --- a/SW.Bitween.Api/Domain/Xchange/XchangeSource.cs +++ b/SW.Bitween.Api/Domain/Xchange/XchangeSource.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Domain /// /// The exchange whose delivery's response this one runs on. /// - /// Values read from the original document, by path. Null until they are read — a message off the + /// Values read from the original document, by path. Null until they are read - a message off the /// bus carries only the id, and each route reads what it uses. /// public record XchangeSource(string XchangeId, IReadOnlyDictionary Values = null); diff --git a/SW.Bitween.Api/Domain/XchangeNotification.cs b/SW.Bitween.Api/Domain/XchangeNotification.cs index 97e8094b..d5f1f60e 100644 --- a/SW.Bitween.Api/Domain/XchangeNotification.cs +++ b/SW.Bitween.Api/Domain/XchangeNotification.cs @@ -23,11 +23,17 @@ public XchangeNotification(string xchangeId, int? notifierId, string notifierNam /// /// Logs an attempt to deliver a "retry budget exhausted" alert. These rows have no /// because the alert is configured on the retry policy rather than - /// on a notifier — which is also how the send is recognised as already done on a redelivery. + /// on a notifier - which is also how the send is recognised as already done on a redelivery. /// public static XchangeNotification ForRetryBudgetAlert(string xchangeId, string exception = null) => new(xchangeId, null, RetryBudgetAlertName, exception); + public const string AutoPauseAlertName = "Auto-pause alert"; + + /// The alert that the subscription this exchange belongs to was paused by its failure. + public static XchangeNotification ForAutoPauseAlert(string xchangeId, string exception = null) => + new(xchangeId, null, AutoPauseAlertName, exception); + public string XchangeId { get; private set; } public bool Success { get; set; } @@ -39,5 +45,11 @@ public static XchangeNotification ForRetryBudgetAlert(string xchangeId, string e public string Exception { get; private set; } public DateTime FinishedOn { get; private set; } + + /// + /// Not sent: the notifier had already sent for this subscription and outcome within its + /// quiet window. Counted into the next notification that is sent. + /// + public bool Suppressed { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/XchangePromotedProperties.cs b/SW.Bitween.Api/Domain/XchangePromotedProperties.cs index f3638f9b..b7660abf 100644 --- a/SW.Bitween.Api/Domain/XchangePromotedProperties.cs +++ b/SW.Bitween.Api/Domain/XchangePromotedProperties.cs @@ -11,7 +11,7 @@ public class XchangePromotedProperties : BaseEntity /// The most a promoted value is stored as. A longer one is cut, ending in "…" to show it. /// /// - /// Promoted values find and name an exchange — an order number, a customer — and they are + /// Promoted values find and name an exchange - an order number, a customer - and they are /// shown on the Exchanges page and name its archived files. A path that picks up a notes /// field or a whole object would put thousands of characters in all of those. Routing is /// not affected: match expressions read the payload, not these. diff --git a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs index ed666dee..99eabc51 100644 --- a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs +++ b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs @@ -45,6 +45,27 @@ public XchangeResult(string xchangeId,WorkGroup workGroup, XchangeFile outputFil }); } + /// + /// Which node processed it, which versions of the mapper and handler ran (the pinned one, or + /// the current one at the time), and how long it took from arriving to finishing. Null on + /// results from before these were recorded. + /// + public string NodeName { get; private set; } + public string MapperVersion { get; private set; } + public string HandlerVersion { get; private set; } + public int? DurationMs { get; private set; } + + public void Stamp(string nodeName, string mapperVersion, string handlerVersion, long durationMs) + { + NodeName = Cut(nodeName, 100); + MapperVersion = Cut(mapperVersion, 50); + HandlerVersion = Cut(handlerVersion, 50); + DurationMs = (int)System.Math.Clamp(durationMs, 0, int.MaxValue); + } + + static string Cut(string value, int length) => + string.IsNullOrEmpty(value) ? null : value.Length <= length ? value : value[..length]; + public bool Success { get; private set; } public string Exception { get; private set; } public DateTime FinishedOn { get; private set; } diff --git a/SW.Bitween.Api/Extensions/AccountExtensions.cs b/SW.Bitween.Api/Extensions/AccountExtensions.cs index e160fd12..d20c02ed 100644 --- a/SW.Bitween.Api/Extensions/AccountExtensions.cs +++ b/SW.Bitween.Api/Extensions/AccountExtensions.cs @@ -16,52 +16,87 @@ namespace SW.Bitween { public static class AccountExtensions { - public static Task GetEmailFromAzureJwtDefault(this UserLogin model) - => model.GetEmailFromAzureJwtDefault(null); + /// Who a Microsoft ID token says it is: the address to match, and the identity to bind. + public record MicrosoftIdentity(string Email, string ObjectAndTenant); - public static async Task GetEmailFromAzureJwtDefault(this UserLogin model, ILogger logger) + // Cached and refreshed by the manager itself. It used to be built on every sign-in, which + // fetched Microsoft's metadata and keys each time. + private static readonly ConfigurationManager MicrosoftMetadata = new( + "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()); + + /// + /// Where Microsoft's signing keys come from: its published metadata. Replaced only by tests, + /// which sign their own tokens to take sign-in through the whole login handler. + /// + internal static Func>> MicrosoftSigningKeys { get; set; } = + async () => (await MicrosoftMetadata.GetConfigurationAsync()).SigningKeys; + + /// + /// Validates a Microsoft ID token for this Bitween's own app registration, or returns null. + /// + /// + /// The audience and issuer used to go unchecked, so a token Microsoft issued to any app, in + /// any tenant, signed in here - one a user handed to some unrelated site would do - and the + /// account was chosen by preferred_username, which Microsoft says not to authorize on. + /// + public static async Task ValidateMicrosoftTokenAsync(string jwt, string clientId, + string tenantId, ILogger logger) { + if (string.IsNullOrEmpty(jwt) || string.IsNullOrWhiteSpace(clientId)) return null; try { - var jwt = model.MsToken; - if (string.IsNullOrEmpty(jwt)) - { - logger?.LogWarning("GetEmailFromAzureJwtDefault: MsToken is null or empty."); - return null; - } - - var configurationManager = new ConfigurationManager( - "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever() - ); - var openIdConfig = await configurationManager.GetConfigurationAsync(); - var handler = new JwtSecurityTokenHandler(); - var tokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = openIdConfig.SigningKeys - }; - handler.ValidateToken(jwt, tokenValidationParameters, out _); - var jwtSecurityToken = handler.ReadJwtToken(jwt); - - var allClaims = string.Join(", ", jwtSecurityToken.Claims.Select(c => $"{c.Type}={c.Value}")); - logger?.LogInformation("MS token claims: {Claims}", allClaims); - - var email = jwtSecurityToken!.Claims.FirstOrDefault(i => i.Type.Contains("preferred_username"))?.Value; - logger?.LogInformation("MS token preferred_username claim value: '{Email}'", email ?? "(not found)"); - return string.IsNullOrWhiteSpace(email) ? null : email; + return ValidateMicrosoftToken(jwt, await MicrosoftSigningKeys(), clientId, tenantId); } catch (Exception ex) { - logger?.LogError(ex, "GetEmailFromAzureJwtDefault: failed to extract email from MS token."); + logger?.LogWarning("Microsoft sign-in refused: {Reason}", ex.Message); return null; } } + /// The checks themselves, against the given keys, so they can be tested. + public static MicrosoftIdentity ValidateMicrosoftToken(string jwt, IEnumerable signingKeys, + string clientId, string tenantId) + { + var handler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + var parameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeys = signingKeys, + ValidateLifetime = true, + ValidateAudience = true, + ValidAudience = clientId, + ValidateIssuer = true, + IssuerValidator = (issuer, token, _) => + { + var tid = ((JwtSecurityToken)token).Claims.FirstOrDefault(c => c.Type == "tid")?.Value; + var fromTenant = tid is not null && + (issuer == $"https://login.microsoftonline.com/{tid}/v2.0" || + issuer == $"https://sts.windows.net/{tid}/"); + // A tenant given as an id pins sign-in to it; "common", "organizations" or a + // domain name can't be compared with the token's tid and leave it open. + var rightTenant = !Guid.TryParse(tenantId, out var pinned) || + string.Equals(tid, pinned.ToString(), StringComparison.OrdinalIgnoreCase); + if (!fromTenant || !rightTenant) + throw new SecurityTokenInvalidIssuerException("The token was not issued by the expected tenant."); + return issuer; + } + }; + + handler.ValidateToken(jwt, parameters, out var validated); + var claims = ((JwtSecurityToken)validated).Claims.ToList(); + + var email = claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value + ?? claims.FirstOrDefault(c => c.Type == "email")?.Value; + var oid = claims.FirstOrDefault(c => c.Type == "oid")?.Value; + var tid = claims.FirstOrDefault(c => c.Type == "tid")?.Value; + if (string.IsNullOrWhiteSpace(email) || oid is null || tid is null) return null; + + return new MicrosoftIdentity(email.ToLowerInvariant(), $"{oid}@{tid}"); + } + private static ClaimsIdentity CreateClaimsIdentity(this Account account, LoginMethod loginMethod) { var claims = new List diff --git a/SW.Bitween.Api/Extensions/IReadOnlyDictionaryExtensions.cs b/SW.Bitween.Api/Extensions/IReadOnlyDictionaryExtensions.cs index b88c9171..21646fca 100644 --- a/SW.Bitween.Api/Extensions/IReadOnlyDictionaryExtensions.cs +++ b/SW.Bitween.Api/Extensions/IReadOnlyDictionaryExtensions.cs @@ -29,7 +29,7 @@ public static ICollection ToKeyAndValueCollection( } /// - /// An exchange's promoted values in the order its information type lists the properties — the + /// An exchange's promoted values in the order its information type lists the properties - the /// order they're shown in, and the first one names an archived exchange. The stored values can't /// be trusted to keep it (Postgres re-sorts a jsonb object's keys), and the type's order can /// change after the exchange ran. Values for properties the type no longer has go last. diff --git a/SW.Bitween.Api/Extensions/InfolinkDbContextExtensions.cs b/SW.Bitween.Api/Extensions/InfolinkDbContextExtensions.cs index a1c424a3..ff42f91a 100644 --- a/SW.Bitween.Api/Extensions/InfolinkDbContextExtensions.cs +++ b/SW.Bitween.Api/Extensions/InfolinkDbContextExtensions.cs @@ -29,8 +29,14 @@ static class BitweenDbContextExtensions if (string.IsNullOrEmpty(partnerKey)) return (false, null, null); + // Stored hashed. The plain form still matches a row the startup pass has not converted + // yet, so an upgrade does not lock anyone out for the moment between the two - but never + // for a value in the stored form itself, or the hash out of a copied database would be + // a working key. Real keys never look like one. + var hashedKey = PartnerKeyHash.Of(partnerKey); + var plainKey = PartnerKeyHash.IsHashed(partnerKey) ? hashedKey : partnerKey; var partnerQuery = from partner in dbContext.Set() - where partner.ApiCredentials.Any(cred => cred.Key == partnerKey) + where partner.ApiCredentials.Any(cred => cred.Key == hashedKey || cred.Key == plainKey) select partner; var par = await partnerQuery.AsNoTracking().SingleOrDefaultAsync(); @@ -40,7 +46,8 @@ where partner.ApiCredentials.Any(cred => cred.Key == partnerKey) // The database may compare without regard to case (SQL Server and MySQL often do), so // the row found can hold a key that only matches ignoring case. Keys are exact. var credential = par.ApiCredentials - .SingleOrDefault(c => string.Equals(c.Key, partnerKey, StringComparison.Ordinal)); + .SingleOrDefault(c => string.Equals(c.Key, hashedKey, StringComparison.Ordinal) || + string.Equals(c.Key, plainKey, StringComparison.Ordinal)); if (credential == null) return (false, null, null); if (keyName != null && !string.Equals(keyName, credential.Name, StringComparison.OrdinalIgnoreCase)) @@ -88,7 +95,7 @@ where partner.ApiCredentials.Any(cred => cred.Key == partnerKey) return (null, null); } - // The username is everything before the first colon — Basic has no way to escape one. + // The username is everything before the first colon - Basic has no way to escape one. var colon = decoded.IndexOf(':'); return colon < 0 ? (null, null) : (decoded[(colon + 1)..], decoded[..colon]); } diff --git a/SW.Bitween.Api/Extensions/OperatorActions.cs b/SW.Bitween.Api/Extensions/OperatorActions.cs new file mode 100644 index 00000000..c4684943 --- /dev/null +++ b/SW.Bitween.Api/Extensions/OperatorActions.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +public static class OperatorActions +{ + /// Adds an operator action to be saved with the caller's next save. + public static void Record(this BitweenDbContext dbContext, RequestContext requestContext, + string action, string target, string detail = null) => + dbContext.Add(new OperatorAction(action, target, detail, requestContext.GetNameIdentifier())); + + /// Records and saves at once, for an action that saves nothing else of its own. + public static async Task RecordNowAsync(this BitweenDbContext dbContext, RequestContext requestContext, + string action, string target, string detail = null) + { + dbContext.Record(requestContext, action, target, detail); + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs index cb3ccc06..897c29ef 100644 --- a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs +++ b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs @@ -28,8 +28,8 @@ public static class RequestContextExtensions /// /// Present on a token issued to an account whose password nobody has chosen. Such a token - /// authenticates but grants nothing, so the account can reach self-service — changing the - /// password — and nothing else. + /// authenticates but grants nothing, so the account can reach self-service - changing the + /// password - and nothing else. /// /// /// A sign-in has to succeed for the password to be changeable at all: the change requires @@ -40,12 +40,10 @@ public static class RequestContextExtensions public const string MustChangePasswordClaim = "bitween_must_change_password"; /// - /// Throws unless the caller holds at least one of . This is really a - /// "forbidden" — the caller is signed in and simply isn't allowed — but CqApi renders - /// SWForbiddenException as a 401 that's byte-identical to sending no token, so there is no - /// distinction to be had on the wire. The client answers a 401 by refreshing the token and - /// retrying once, which means every denial costs a wasted round trip. Worth revisiting if - /// CqApi ever maps forbidden to 403. + /// Throws unless the caller holds at least one of . CqApi renders + /// the exception as a 401; for a signed-in caller the web host rewrites that to 403 on the + /// way out (see Startup), so a client can tell "not allowed" from "not signed in" and does + /// not spend a token refresh on a refusal. /// public static async Task EnsurePermission(this RequestContext requestContext, BitweenDbContext dbContext, params string[] anyOf) @@ -82,6 +80,11 @@ public static async Task> GetPermissions(this RequestContext req if (!int.TryParse(requestContext.GetNameIdentifier(), out var accountId)) throw new SWUnauthorizedException("INSUFFICIENT_PERMISSIONS"); + // A token outlives the account's state: disabling someone has to take effect now, not + // when their last token expires. Same for an account that has since been removed. + if (!await dbContext.Set().AnyAsync(a => a.Id == accountId && !a.Disabled && !a.Deleted)) + return []; + return await GetPermissionsOf(dbContext, accountId); } diff --git a/SW.Bitween.Api/Helpers/StartupValuesFiller.cs b/SW.Bitween.Api/Helpers/StartupValuesFiller.cs index 7c7c13ca..b1ebb2b5 100644 --- a/SW.Bitween.Api/Helpers/StartupValuesFiller.cs +++ b/SW.Bitween.Api/Helpers/StartupValuesFiller.cs @@ -12,7 +12,7 @@ public static class StartupValuesFiller /// How the pipeline tells a runtime that this subscription runs through a data source, without /// every call site along the way having to grow a parameter for it. /// - /// Reserved, and stripped before the adapter ever sees it — the adapter's own settings come + /// Reserved, and stripped before the adapter ever sees it - the adapter's own settings come /// from the data source itself. The double-underscore convention matches the /// __partner__ and __globals__ injections the mapper already relies on. /// @@ -23,7 +23,7 @@ public static class StartupValuesFiller /// /// Unlike this is NOT stripped: the adapter reads it. A resident /// data source is one instance shared by every subscription pointed at it, so anything the - /// adapter remembers between calls — a receive cursor above all — has to be namespaced by the + /// adapter remembers between calls - a receive cursor above all - has to be namespaced by the /// reader, or two subscriptions polling one connection share one cursor and each sees half the /// rows. The adapter side of this contract is DbReceiver's CursorStateName. /// @@ -66,8 +66,8 @@ public static Dictionary WithSubscription(this Dictionary /// Fills {{source.PATH}} from the values read out of the original document: the input of - /// the exchange whose delivery got the response this one runs on. Null values leave the tokens as they are — every exchange but - /// a response or bus gateway subscription's — and so does a path with no value, for + /// the exchange whose delivery got the response this one runs on. Null values leave the tokens as they are - every exchange but + /// a response or bus gateway subscription's - and so does a path with no value, for /// to find. /// public static Dictionary FillSource(this Dictionary properties, diff --git a/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs b/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs index 6f6117e5..18849e61 100644 --- a/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs +++ b/SW.Bitween.Api/Resources/Accounts/AccountRoles.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Resources.Accounts; internal static class AccountRoles { - /// Role summaries for a set of accounts, in one query — keyed by account id. + /// Role summaries for a set of accounts, in one query - keyed by account id. public static async Task>> For(BitweenDbContext dbContext, List accountIds) { @@ -57,7 +57,7 @@ public static async Task Set(BitweenDbContext dbContext, int accountId, List /// Keeps the legacy column meaningful for older API consumers. It no - /// longer drives authorization — it's derived from whichever built-in role the member holds, + /// longer drives authorization - it's derived from whichever built-in role the member holds, /// falling back to Member for someone who only holds custom roles. /// public static AccountRole LegacyRoleFor(List roleIds) diff --git a/SW.Bitween.Api/Resources/Accounts/Administrators.cs b/SW.Bitween.Api/Resources/Accounts/Administrators.cs index fbd18109..928faac5 100644 --- a/SW.Bitween.Api/Resources/Accounts/Administrators.cs +++ b/SW.Bitween.Api/Resources/Accounts/Administrators.cs @@ -9,13 +9,15 @@ namespace SW.Bitween.Resources.Accounts; internal static class Administrators { /// - /// How many other enabled accounts still hold the Administrator role. Guards every operation - /// that could otherwise leave an instance with nobody able to manage members and roles. + /// How many other enabled people still hold the Administrator role. Guards every operation + /// that could otherwise leave an instance with nobody able to manage members and roles. A + /// service account holding it doesn't count: no person could sign in as it to put things right. /// public static Task OtherThan(BitweenDbContext dbContext, int accountId) => (from link in dbContext.Set() join account in dbContext.Set() on link.AccountId equals account.Id - where link.RoleId == Role.AdministratorId && link.AccountId != accountId && !account.Disabled + where link.RoleId == Role.AdministratorId && link.AccountId != accountId && !account.Disabled && + account.Kind == AccountKind.Person select link.AccountId).CountAsync(); public static Task Holds(BitweenDbContext dbContext, int accountId) => @@ -24,7 +26,7 @@ public static Task Holds(BitweenDbContext dbContext, int accountId) => /// /// Blocks an operation that would remove the last administrator. Only applies when the account - /// actually holds the role — otherwise nothing is being taken away. + /// actually holds the role - otherwise nothing is being taken away. /// public static async Task EnsureNotTheLast(BitweenDbContext dbContext, int accountId) { diff --git a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs index 3c13599c..f7011d23 100644 --- a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs +++ b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs @@ -18,9 +18,11 @@ public async Task Handle(ChangePasswordModel request) // Self-service: this only ever changes the caller's own password, and the old one has to // be supplied. The guard it replaces listed every role, so it granted nothing. var accountId = Convert.ToInt32(requestContext.GetNameIdentifier()); - var account = await dbContext.Set().FindAsync(accountId); + var account = await Members.FindAsync(dbContext, accountId); - if (!SecurePasswordHasher.Verify(request.OldPassword, account!.Password)) + // A service account has no password to change, and isn't found here. + if (account is null || string.IsNullOrEmpty(account.Password) || + !SecurePasswordHasher.Verify(request.OldPassword, account.Password)) { throw new SWValidationException("AUTHENTICATION_ERROR", "The Password entered does not match the user's password"); @@ -29,7 +31,7 @@ public async Task Handle(ChangePasswordModel request) account.SetPassword(request.NewPassword); await dbContext.SaveChangesAsync(); - // Signing in with a refresh token skips password verification entirely — it looks the + // Signing in with a refresh token skips password verification entirely - it looks the // account up by id and issues a fresh token from whatever state it is now in. So a session // opened with the old password outlives the change unless the tokens go with it, and // "change the password" would not actually remove whoever you changed it because of. diff --git a/SW.Bitween.Api/Resources/Accounts/CliGrant.cs b/SW.Bitween.Api/Resources/Accounts/CliGrant.cs new file mode 100644 index 00000000..3ab6b5ec --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/CliGrant.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +public class CliGrantRequest +{ + /// The CLI's S256 code challenge. + public string CodeChallenge { get; set; } +} + +/// +/// A signed-in member signing the bitween CLI in as themselves, from the admin UI's confirmation +/// page: the code it returns goes back to the CLI, which redeems it at . It +/// works whichever way the member signed in to the browser, Microsoft included. An account that +/// must change its password never gets here; the pipeline refuses its token everywhere else. +/// +[HandlerName("cligrant")] +public class CliGrant(RequestContext requestContext, CliSignInCodes codes) : ICommandHandler +{ + public Task Handle(CliGrantRequest request) + { + if (!CliSignInCodes.IsChallenge(request?.CodeChallenge)) + throw new SWValidationException("CodeChallenge", "This isn't a sign-in request from the bitween CLI."); + + var accountId = Convert.ToInt32(requestContext.GetNameIdentifier()); + return Task.FromResult(new { Code = codes.Issue(accountId, request.CodeChallenge, DateTimeOffset.UtcNow) }); + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/CliToken.cs b/SW.Bitween.Api/Resources/Accounts/CliToken.cs new file mode 100644 index 00000000..86b34ada --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/CliToken.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain.Accounts; +using SW.HttpExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +public class CliTokenRequest +{ + public string Code { get; set; } + public string CodeVerifier { get; set; } +} + +/// +/// Where the bitween CLI trades the code from , and the verifier only it +/// holds, for a session: an access token and a refresh token, both in the body since the CLI keeps +/// no cookies. The refresh token renews and ends like the browser's (see ). +/// +[HandlerName("clitoken")] +[Unprotect] +public class CliToken(BitweenDbContext dbContext, JwtTokenParameters jwtTokenParameters, BitweenOptions options, + CliSignInCodes codes, ILogger logger) : ICommandHandler +{ + public async Task Handle(CliTokenRequest request) + { + var accountId = codes.Redeem(request?.Code, request?.CodeVerifier, DateTimeOffset.UtcNow); + var account = accountId is null ? null : await Members.FindAsync(dbContext, accountId.Value); + + // Disabled or told to change its password since the code was issued: no session either way. + if (account is null || account.Disabled || account.MustChangePassword) + { + logger.LogWarning("CLI sign-in refused: the code was invalid, expired, or its account can't sign in."); + throw new SWException("This sign-in has expired or isn't valid. Run bitween login again."); + } + + account.SignedIn(); + var refreshToken = new RefreshToken(account.Id, LoginMethod.EmailAndPassword); + dbContext.Add(refreshToken); + await dbContext.SaveChangesAsync(); + + return new + { + Jwt = account.CreateJwt(LoginMethod.EmailAndPassword, jwtTokenParameters, TimeSpan.FromMinutes(options.JwtExpiryMinutes)), + RefreshToken = refreshToken.Id, + account.Email, + }; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Create.cs b/SW.Bitween.Api/Resources/Accounts/Create.cs index 284c8f7e..f9f2f1d5 100644 --- a/SW.Bitween.Api/Resources/Accounts/Create.cs +++ b/SW.Bitween.Api/Resources/Accounts/Create.cs @@ -27,7 +27,7 @@ public async Task Handle(CreateAccountModel request) throw new SWValidationException("ACCOUNT_EXISTS", $"Account with email {request.Email} exists"); // Prefer explicit role ids, and fall back to the legacy coarse role only when one was - // actually sent. It used to be a plain int, so omitting it meant 0 — which is Admin — + // actually sent. It used to be a plain int, so omitting it meant 0 - which is Admin - // and adding a member with no roles ticked handed them the run of the instance. var roleIds = request.RoleIds is { Count: > 0 } ? request.RoleIds.Distinct().ToList() @@ -35,7 +35,9 @@ public async Task Handle(CreateAccountModel request) ? [] : [BuiltInRoleFor((AccountRole)request.Role)]; - // No password at all when this instance signs in through Microsoft only — the account + await GrantLimits.EnsureCallerHoldsRoles(dbContext, requestContext, roleIds); + + // No password at all when this instance signs in through Microsoft only - the account // exists purely to be matched by email. var password = _bitweenOptions.DisableEmailPasswordLogin ? null @@ -71,7 +73,7 @@ public Validate(BitweenOptions bitweenOptions) RuleFor(i => i.Password).Password().When(_ => !bitweenOptions.DisableEmailPasswordLogin); // The handler prefers explicit role ids and only falls back to the legacy coarse // Role, so demanding Role unconditionally rejected the payload the UI actually - // sends — adding a member failed outright. Still required when no ids were sent, + // sends - adding a member failed outright. Still required when no ids were sent, // which is what keeps an account from being created with no roles at all. RuleFor(i => i.Role).NotNull().When(i => i.RoleIds is not { Count: > 0 }); } diff --git a/SW.Bitween.Api/Resources/Accounts/GrantLimits.cs b/SW.Bitween.Api/Resources/Accounts/GrantLimits.cs new file mode 100644 index 00000000..520d58ff --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/GrantLimits.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +/// +/// Nobody hands out more than they hold. Managing members and roles is delegable - a custom role +/// can carry users.edit or roles.edit without being Administrator - and without this a holder of +/// either could grant themselves everything: put themselves in Administrator, add every permission +/// to a role they hold, or set an administrator's password and sign in as them. +/// +/// +/// The rule is a subset check against the caller's own grants, resolved the same way +/// resolves them for every guard. An +/// administrator holds the whole catalog, so nothing an administrator could do before is refused. +/// +internal static class GrantLimits +{ + /// Refuses unless the caller holds every permission grant. + public static async Task EnsureCallerHoldsRoles(BitweenDbContext dbContext, RequestContext requestContext, + IEnumerable roleIds) + { + var ids = (roleIds ?? []).Distinct().ToList(); + if (ids.Count == 0) return; + + var roles = await dbContext.Set().AsNoTracking().Where(r => ids.Contains(r.Id)).ToListAsync(); + await EnsureCallerHoldsPermissions(dbContext, requestContext, + roles.SelectMany(r => r.GetEffectivePermissions() ?? [])); + } + + /// Refuses unless the caller holds every one of . + public static async Task EnsureCallerHoldsPermissions(BitweenDbContext dbContext, RequestContext requestContext, + IEnumerable permissions) + { + var granted = await requestContext.GetPermissions(dbContext); + var beyond = (permissions ?? []).Distinct().Where(p => !granted.Contains(p)).OrderBy(p => p).ToList(); + if (beyond.Count > 0) + throw new SWValidationException("BEYOND_YOUR_PERMISSIONS", + "You can only grant permissions you hold yourself. Not held: " + string.Join(", ", beyond) + "."); + } + + /// + /// Refuses unless the caller holds everything the account holds - so a member can't reset, + /// re-role, disable or remove someone with more access than they have. + /// + public static async Task EnsureCallerOutranks(BitweenDbContext dbContext, RequestContext requestContext, + int accountId) + { + var target = await RequestContextExtensions.GetPermissionsOf(dbContext, accountId); + var granted = await requestContext.GetPermissions(dbContext); + if (!target.IsSubsetOf(granted)) + throw new SWValidationException("ACCOUNT_OUTRANKS_YOU", + "This member holds permissions you don't, so you can't change their account."); + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Login.cs b/SW.Bitween.Api/Resources/Accounts/Login.cs index 1e8d1152..c50426a3 100644 --- a/SW.Bitween.Api/Resources/Accounts/Login.cs +++ b/SW.Bitween.Api/Resources/Accounts/Login.cs @@ -14,10 +14,20 @@ namespace SW.Bitween.Resources.Accounts [HandlerName("login")] [Unprotect] public class Login(JwtTokenParameters jwtTokenParameters, BitweenDbContext dbContext, - BitweenOptions BitweenSettings, IHttpContextAccessor httpContextAccessor, ILogger logger) : ICommandHandler + BitweenOptions BitweenSettings, IHttpContextAccessor httpContextAccessor, ILogger logger, + SignInThrottle throttle) : ICommandHandler { - private const int MaxFailedLoginAttempts = 5; + // Account-wide, across every address: the backstop for guessing spread over many of them. + // Five from any one address already locks that address out - see SignInThrottle. + private const int MaxFailedLoginAttempts = 20; private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15); + private static readonly TimeSpan RefreshTokenLifetime = TimeSpan.FromDays(30); + private static readonly TimeSpan RefreshGrace = TimeSpan.FromSeconds(30); + + // Verified against when no account matches, so an unknown email costs the same PBKDF2 work + // as a known one. Without it the response time alone said which addresses have accounts. + private static readonly Lazy UnknownAccountHash = + new(() => SecurePasswordHasher.Hash(Guid.NewGuid().ToString("N"))); public async Task Handle(UserLogin request) { @@ -26,17 +36,62 @@ public async Task Handle(UserLogin request) var accountQ = dbContext .Set() .AsQueryable(); + AccountExtensions.MicrosoftIdentity microsoftIdentity = null; + + // Credentials sent with the request are what the caller is asking to sign in as. A refresh + // cookie can still be in the browser from the previous session - a sign-out whose request + // never completed leaves it there - and preferring it signed the next person in as the + // previous one. So the cookie is only read when nothing else was sent. + var signingInWithCredentials = !string.IsNullOrEmpty(request.MsToken) || + !string.IsNullOrEmpty(request.Username) || + !string.IsNullOrEmpty(request.Password); - // Prefer refresh token from HttpOnly cookie (secure), fall back to body (legacy) - var refreshTokenValue = httpContextAccessor.HttpContext?.Request.Cookies["refresh_token"]; - if (string.IsNullOrEmpty(refreshTokenValue)) + // Refresh token from the HttpOnly cookie (secure), falling back to the body (legacy). + var refreshTokenValue = signingInWithCredentials + ? null + : httpContextAccessor.HttpContext?.Request.Cookies["refresh_token"]; + if (string.IsNullOrEmpty(refreshTokenValue) && !signingInWithCredentials) refreshTokenValue = request.RefreshToken; if (!string.IsNullOrEmpty(refreshTokenValue)) { - var refreshToken = await dbContext.Set() + var refreshToken = await dbContext.Set().AsNoTracking() .SingleOrDefaultAsync(x => x.Id == refreshTokenValue); - if (refreshToken is null) + var now = DateTime.UtcNow; + + // Marked superseded rather than deleted, with one conditional UPDATE so two requests + // with the same token can't both be the first. A token superseded moments ago still + // works, for the browser whose copy of the new one went missing; one superseded longer + // ago is refused. Deleting it outright used to make the loser of a race throw a + // concurrency error - a 500 from the sign-in endpoint - and a lost response sign out. + var spent = false; + if (refreshToken is not null) + { + spent = await dbContext.Set() + .Where(t => t.Id == refreshTokenValue && t.SupersededOn == null) + .ExecuteUpdateAsync(u => u.SetProperty(t => t.SupersededOn, now)) == 1; + + if (!spent) + spent = await dbContext.Set().AsNoTracking() + .AnyAsync(t => t.Id == refreshTokenValue && t.SupersededOn > now - RefreshGrace); + + // Superseded ones past their grace are of no further use to anyone. + var graceEnded = now - RefreshGrace; + await dbContext.Set() + .Where(t => t.AccountId == refreshToken.AccountId && t.SupersededOn < graceEnded) + .ExecuteDeleteAsync(); + } + + // The cookie expires after 30 days, but the row did not, and a refresh token sent in + // the body has no cookie to expire. Each refresh issues a new token, so this is an + // idle limit: a session in use keeps renewing, one left alone for 30 days ends. + if (spent && refreshToken.CreatedOn < now - RefreshTokenLifetime) + { + logger.LogInformation("Refresh token older than {Days} days refused.", RefreshTokenLifetime.TotalDays); + spent = false; + } + + if (!spent) { logger.LogWarning("Refresh token not found in DB, clearing cookie and falling back to credentials."); httpContextAccessor.HttpContext?.Response.Cookies.Delete("refresh_token"); @@ -44,10 +99,16 @@ public async Task Handle(UserLogin request) } else { - dbContext.Remove(refreshToken); accountQ = accountQ.Where(u => u.Id == refreshToken.AccountId); } } + else if (signingInWithCredentials && + httpContextAccessor.HttpContext?.Request.Cookies["refresh_token"] is { Length: > 0 } leftover) + { + // The previous session's token, left behind: it is replaced below, so it goes now + // rather than lingering as a second way in. + await dbContext.Set().Where(t => t.Id == leftover).ExecuteDeleteAsync(); + } if (string.IsNullOrEmpty(refreshTokenValue) && string.IsNullOrEmpty(request.MsToken) && BitweenSettings.DisableEmailPasswordLogin) @@ -72,14 +133,27 @@ public async Task Handle(UserLogin request) } else if (!string.IsNullOrEmpty(request.MsToken)) { - var email = (await request.GetEmailFromAzureJwtDefault(logger))?.ToLower(); - if (string.IsNullOrEmpty(email)) + if (string.IsNullOrWhiteSpace(BitweenSettings.MsalClientId)) + throw new SWException("Microsoft sign-in is not configured."); + + microsoftIdentity = await AccountExtensions.ValidateMicrosoftTokenAsync( + request.MsToken, BitweenSettings.MsalClientId, BitweenSettings.MsalTenantId, logger); + if (microsoftIdentity is null) { - logger.LogWarning("MS login failed: could not extract email from token."); - throw new SWException("Could not retrieve your email from Microsoft. Please ensure your Microsoft account has a valid email address and try again."); + logger.LogWarning("MS login failed: the token was not valid for this application."); + throw new SWException("Could not sign you in with Microsoft. Please try again, or contact your administrator."); } - logger.LogInformation("MS login attempt. Extracted email from token: '{Email}'", email); - accountQ = accountQ.Where(u => u.Email.ToLower() == email); + + // The bound identity first; the address only for an account that has none yet. + var identity = microsoftIdentity.ObjectAndTenant; + var email = microsoftIdentity.Email; + var accountId = await dbContext.Set().AsNoTracking() + .Where(u => u.MicrosoftIdentity == identity) + .Select(u => (int?)u.Id).FirstOrDefaultAsync() + ?? await dbContext.Set().AsNoTracking() + .Where(u => u.Email.ToLower() == email) + .Select(u => (int?)u.Id).FirstOrDefaultAsync(); + accountQ = accountQ.Where(u => u.Id == accountId); } else { @@ -89,8 +163,17 @@ public async Task Handle(UserLogin request) var account = await accountQ .SingleOrDefaultAsync(); + // Service accounts sign in only with their tokens. They have no email or password to + // match, so this is the second lock on that door, not the first. + if (account is { IsService: true }) account = null; + + var credentialLogin = string.IsNullOrEmpty(refreshTokenValue) && string.IsNullOrEmpty(request.MsToken); + if (account is null) { + if (credentialLogin) + SecurePasswordHasher.Verify(request.Password, UnknownAccountHash.Value); + if (!string.IsNullOrEmpty(request.MsToken)) { logger.LogWarning("MS login failed: no account found matching the token email."); @@ -100,7 +183,9 @@ public async Task Handle(UserLogin request) throw new SWException("Invalid username or password."); } - if (account.Disabled) + // A password sign-in learns the account is disabled only once the password is right - + // see below - so the message can't be used to find out which addresses exist. + if (account.Disabled && !credentialLogin) { if (!string.IsNullOrEmpty(request.MsToken)) { @@ -111,20 +196,35 @@ public async Task Handle(UserLogin request) throw new SWException("Your account has been disabled. Please contact your administrator."); } + if (microsoftIdentity is not null) + { + if (account.MicrosoftIdentity is null) + account.BindMicrosoftIdentity(microsoftIdentity.ObjectAndTenant); + else if (account.MicrosoftIdentity != microsoftIdentity.ObjectAndTenant) + { + logger.LogWarning("MS login refused: account {AccountId} is bound to a different Microsoft identity.", account.Id); + throw new SWException("This account is linked to a different Microsoft account. Please contact your administrator."); + } + } + if (string.IsNullOrEmpty(refreshTokenValue) && !string.IsNullOrEmpty(request.Username) && !string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.MsToken)) { var nowUtc = DateTime.UtcNow; - if (account.IsLockedOut(nowUtc)) + var address = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var lockedUntil = new[] { account.IsLockedOut(nowUtc) ? account.LockoutEnd : null, + throttle.LockedUntil(account.Id, address, nowUtc) } + .Max(); + if (lockedUntil is not null) { - var minutes = (int)Math.Ceiling((account.LockoutEnd!.Value - nowUtc).TotalMinutes); + var minutes = (int)Math.Ceiling((lockedUntil.Value - nowUtc).TotalMinutes); logger.LogWarning("Login rejected: account '{Email}' is temporarily locked.", account.Email); throw new SWException( $"Your account is temporarily locked due to multiple failed login attempts. " + $"Please try again in {minutes} minute{(minutes == 1 ? "" : "s")}."); } - // An account can have no stored password at all — that is how one created while + // An account can have no stored password at all - that is how one created while // Microsoft-only sign-in was on looks, and that setting can be turned back off. // Verify dereferences the stored hash, so without this the attempt is a 500 from // an unauthenticated endpoint instead of an ordinary failed sign-in. @@ -132,6 +232,8 @@ public async Task Handle(UserLogin request) string.IsNullOrEmpty(account.Password) || !SecurePasswordHasher.Verify(request.Password, account.Password)) { + throttle.RegisterFailure(account.Id, address, nowUtc); + // Atomic DB-side update so concurrent wrong-password attempts can't read the // same count and lose increments, which would let them slip past the lockout. var lockoutEnd = nowUtc.Add(LockoutDuration); @@ -145,13 +247,20 @@ await dbContext.Set() throw new SWException("Invalid username or password."); } + if (account.Disabled) + throw new SWException("Your account has been disabled. Please contact your administrator."); + + throttle.Clear(account.Id, address); account.RegisterSuccessfulLogin(); } + // A sign-in, not a session renewing itself: the member's last sign-in, and the trail's. + if (string.IsNullOrEmpty(refreshTokenValue)) account.SignedIn(); + var newRefreshToken = CreateRefreshToken(account, LoginMethod.EmailAndPassword); await dbContext.SaveChangesAsync(); - // Set refresh token as a secure, HttpOnly cookie — not accessible to JavaScript. + // Set refresh token as a secure, HttpOnly cookie - not accessible to JavaScript. // Secure is always on: the app is served over HTTPS, and TLS is terminated at the // reverse proxy, so Request.IsHttps would otherwise be false and drop the attribute. httpContextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions @@ -159,10 +268,10 @@ await dbContext.Set() HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, - Expires = DateTimeOffset.UtcNow.AddDays(30) + Expires = DateTimeOffset.UtcNow.Add(RefreshTokenLifetime) }); - // Return only the JWT — refresh token stays in the cookie, not in the response body. + // Return only the JWT - refresh token stays in the cookie, not in the response body. // MustChangePassword rides along so the client can send them straight to the change // form; the token grants nothing until they do, so this is a courtesy, not the control. return new diff --git a/SW.Bitween.Api/Resources/Accounts/Logout.cs b/SW.Bitween.Api/Resources/Accounts/Logout.cs index bdb0089d..c0472d21 100644 --- a/SW.Bitween.Api/Resources/Accounts/Logout.cs +++ b/SW.Bitween.Api/Resources/Accounts/Logout.cs @@ -6,17 +6,25 @@ namespace SW.Bitween.Resources.Accounts { - public class UserLogout { } + public class UserLogout + { + /// + /// The refresh token to end, for a client without cookies such as the bitween CLI. Ending + /// one needs only the token itself, which is what renewing a session with it needs too. + /// + public string RefreshToken { get; set; } + } [HandlerName("logout")] [Unprotect] -public class Logout(BitweenDbContext dbContext, IHttpContextAccessor httpContextAccessor) + public class Logout(BitweenDbContext dbContext, IHttpContextAccessor httpContextAccessor) : ICommandHandler { public async Task Handle(UserLogout request) { var httpContext = httpContextAccessor.HttpContext; - var refreshTokenValue = httpContext?.Request.Cookies["refresh_token"]; + var cookie = httpContext?.Request.Cookies["refresh_token"]; + var refreshTokenValue = string.IsNullOrEmpty(cookie) ? request?.RefreshToken : cookie; if (!string.IsNullOrEmpty(refreshTokenValue)) { @@ -29,7 +37,7 @@ public async Task Handle(UserLogout request) await dbContext.SaveChangesAsync(); } - httpContext.Response.Cookies.Delete("refresh_token"); + if (!string.IsNullOrEmpty(cookie)) httpContext.Response.Cookies.Delete("refresh_token"); } // Tell the browser to wipe cookies, cache and storage for this origin on logout. diff --git a/SW.Bitween.Api/Resources/Accounts/Members.cs b/SW.Bitween.Api/Resources/Accounts/Members.cs new file mode 100644 index 00000000..88af1369 --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/Members.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; + +namespace SW.Bitween.Resources.Accounts; + +internal static class Members +{ + /// + /// A person's account by id, or null. Service accounts aren't members: what can be done to them + /// is under service accounts, behind its own permissions, so the member endpoints don't find them. + /// + public static async Task FindAsync(BitweenDbContext dbContext, int id) + { + var account = await dbContext.Set().FindAsync(id); + return account is { IsService: true } ? null : account; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/Profile.cs b/SW.Bitween.Api/Resources/Accounts/Profile.cs index 263a6c94..020aa018 100644 --- a/SW.Bitween.Api/Resources/Accounts/Profile.cs +++ b/SW.Bitween.Api/Resources/Accounts/Profile.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.Accounts; /// -/// Who am I and what may I do. Deliberately ungated — every signed-in caller needs it, and the +/// Who am I and what may I do. Deliberately ungated - every signed-in caller needs it, and the /// permissions it returns are what the UI uses to decide which pages and actions to show. /// [HandlerName("profile")] diff --git a/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs b/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs index bda39d90..97fc3177 100644 --- a/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs +++ b/SW.Bitween.Api/Resources/Accounts/RemoveAccount.cs @@ -13,7 +13,7 @@ public async Task Handle(int key, RemoveAccountModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Delete); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"Account with {key} was not found"); @@ -22,6 +22,7 @@ public async Task Handle(int key, RemoveAccountModel request) throw new SWValidationException("CANNOT_REMOVE_SELF", "You can't remove your own account."); await Administrators.EnsureNotTheLast(dbContext, key); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); dbContext.Remove(account); await dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Accounts/Search.cs b/SW.Bitween.Api/Resources/Accounts/Search.cs index 084e1776..626af326 100644 --- a/SW.Bitween.Api/Resources/Accounts/Search.cs +++ b/SW.Bitween.Api/Resources/Accounts/Search.cs @@ -32,6 +32,10 @@ public async Task Handle(SearchMembersModel request) .ToDictionaryAsync(i => i.Id, i => i.DisplayName); } + // The member list is people; service accounts have their own (lookup keeps them, so a + // change one made still shows who made it). + query = query.Where(a => a.Kind == AccountKind.Person); + var count = await query.CountAsync(); var accounts = await query.OrderBy(i => i.CreatedOn) @@ -45,7 +49,9 @@ public async Task Handle(SearchMembersModel request) Id = a.Id, Disabled = a.Disabled, Role = a.Role.ToString(), - LockoutEnd = a.LockoutEnd + LockoutEnd = a.LockoutEnd, + LastSignInOn = a.LastSignInOn, + MicrosoftLinked = a.MicrosoftIdentity != null }) .ToListAsync(); diff --git a/SW.Bitween.Api/Resources/Accounts/ServiceTokenSignIn.cs b/SW.Bitween.Api/Resources/Accounts/ServiceTokenSignIn.cs new file mode 100644 index 00000000..594b585d --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/ServiceTokenSignIn.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.HttpExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +/// +/// Where a service account signs in: its token, traded for an ordinary short session. Nothing else +/// accepts a token, so everything the session does is checked the way a member's is: permissions +/// from its roles on every call, disabling it ends it at once. +/// +/// +/// No refresh token: the caller holds the token and trades it again when the session runs out. +/// Tokens that match nothing count against the address they came from, as wrong passwords do: that +/// is what guessing looks like. A real token that has expired or been revoked doesn't; that's a job +/// nobody has updated yet, and locking its address out would stop every other job beside it. +/// +[HandlerName("servicetoken")] +[Unprotect] +public class ServiceTokenSignIn(BitweenDbContext dbContext, JwtTokenParameters jwtTokenParameters, BitweenOptions options, + IHttpContextAccessor httpContextAccessor, SignInThrottle throttle, ILogger logger) + : ICommandHandler +{ + /// What the throttle counts unknown tokens against: no account has this id. + internal const int NoAccount = 0; + + const string Refused = "This token isn't valid: it's unknown, expired or revoked, or its account is disabled."; + + public async Task Handle(ServiceTokenSignInModel request) + { + var now = DateTime.UtcNow; + var address = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + + var lockedUntil = throttle.LockedUntil(NoAccount, address, now); + if (lockedUntil is not null) + { + var minutes = (int)Math.Ceiling((lockedUntil.Value - now).TotalMinutes); + throw new SWException($"Too many invalid tokens from this address. Try again in {minutes} minute{(minutes == 1 ? "" : "s")}."); + } + + var value = request?.Token?.Trim(); + var hash = ServiceToken.LooksLikeOne(value) ? ServiceToken.HashOf(value) : null; + var token = hash is null + ? null + : await dbContext.Set().AsNoTracking().SingleOrDefaultAsync(t => t.Hash == hash); + var account = token is null + ? null + : await dbContext.Set().AsNoTracking().SingleOrDefaultAsync(a => a.Id == token.AccountId); + + if (token is null || !token.IsActive(now) || account is null || account.Deleted || account.Disabled || !account.IsService) + { + if (token is null) throttle.RegisterFailure(NoAccount, address, now); + logger.LogWarning("Service token sign-in refused from {Address}: {Reason}.", address, + token is null ? "no such token" : !token.IsActive(now) ? $"token {token.Shown} expired or revoked" : "account disabled"); + throw new SWException(Refused); + } + + // Straight to the row: who used it last is not a change anyone needs in the audit trail. + await dbContext.Set().Where(t => t.Id == token.Id) + .ExecuteUpdateAsync(u => u.SetProperty(t => t.LastUsedOn, now).SetProperty(t => t.LastUsedFrom, address)); + await dbContext.Set().Where(a => a.Id == account.Id) + .ExecuteUpdateAsync(u => u.SetProperty(a => a.LastSignInOn, now)); + + var lifetime = TimeSpan.FromMinutes(options.JwtExpiryMinutes); + return new + { + Jwt = account.CreateJwt(LoginMethod.ApiKey, jwtTokenParameters, lifetime), + ExpiresIn = (int)lifetime.TotalSeconds, + account.Id, + Name = account.DisplayName, + }; + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs b/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs index b88375d6..be332e23 100644 --- a/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs +++ b/SW.Bitween.Api/Resources/Accounts/SetDisabled.cs @@ -1,3 +1,5 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq; using System; using System.Threading.Tasks; using SW.Bitween.Domain.Accounts; @@ -8,7 +10,7 @@ namespace SW.Bitween.Resources.Accounts; /// /// Suspends or restores an account. A disabled account keeps its roles and history but can't sign -/// in — see the Disabled check in the login handler. +/// in - see the Disabled check in the login handler. /// [HandlerName("setDisabled")] public class SetDisabled(BitweenDbContext dbContext, RequestContext requestContext) @@ -18,10 +20,12 @@ public async Task Handle(int key, SetAccountDisabledModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Edit); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + if (request.Disabled) { if (key == Convert.ToInt32(requestContext.GetNameIdentifier())) @@ -33,6 +37,13 @@ public async Task Handle(int key, SetAccountDisabledModel request) account.SetDisabled(request.Disabled); await dbContext.SaveChangesAsync(); + // A refresh token re-issues a Jwt without asking for the password, so a disabled account's + // sessions have to go with it - the same reasoning as SetPassword. + if (request.Disabled) + await dbContext.Set() + .Where(t => t.AccountId == account.Id) + .ExecuteDeleteAsync(); + return null; } } diff --git a/SW.Bitween.Api/Resources/Accounts/SetPassword.cs b/SW.Bitween.Api/Resources/Accounts/SetPassword.cs index d019c85b..e236ed67 100644 --- a/SW.Bitween.Api/Resources/Accounts/SetPassword.cs +++ b/SW.Bitween.Api/Resources/Accounts/SetPassword.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.Resources.Accounts; /// /// Sets a member's password on their behalf. Bitween has no outbound mail, so there's no -/// self-service reset — without this, anyone who forgets their password is locked out for good. +/// self-service reset - without this, anyone who forgets their password is locked out for good. /// Changing your own password goes through ChangePassword, which asks for the current one. /// [HandlerName("setPassword")] @@ -26,14 +26,17 @@ public async Task Handle(int key, SetAccountPasswordModel request) throw new SWValidationException("USE_CHANGE_PASSWORD", "Use Change password to set your own, so the current one is still required."); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + // Setting someone's password is signing in as them, so it needs everything they hold. + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + account.SetPassword(request.Password); await dbContext.SaveChangesAsync(); - // Signing in with a refresh token skips password verification entirely — it looks the + // Signing in with a refresh token skips password verification entirely - it looks the // account up by id and issues a fresh token from whatever state it is now in. So a session // opened with the old password outlives the change unless the tokens go with it, and // "change the password" would not actually remove whoever you changed it because of. diff --git a/SW.Bitween.Api/Resources/Accounts/SetRoles.cs b/SW.Bitween.Api/Resources/Accounts/SetRoles.cs index 693be882..f032dc91 100644 --- a/SW.Bitween.Api/Resources/Accounts/SetRoles.cs +++ b/SW.Bitween.Api/Resources/Accounts/SetRoles.cs @@ -16,13 +16,16 @@ public async Task Handle(int key, SetAccountRolesModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Edit); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); var roleIds = (request.RoleIds ?? []).Distinct().ToList(); - // Don't let the last administrator be demoted — including by themselves. Otherwise an + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + await GrantLimits.EnsureCallerHoldsRoles(dbContext, requestContext, roleIds); + + // Don't let the last administrator be demoted - including by themselves. Otherwise an // instance ends up with nobody able to manage members or roles. if (!roleIds.Contains(Role.AdministratorId)) await Administrators.EnsureNotTheLast(dbContext, key); diff --git a/SW.Bitween.Api/Resources/Accounts/Unlock.cs b/SW.Bitween.Api/Resources/Accounts/Unlock.cs index 829d5829..07a2ccde 100644 --- a/SW.Bitween.Api/Resources/Accounts/Unlock.cs +++ b/SW.Bitween.Api/Resources/Accounts/Unlock.cs @@ -6,20 +6,24 @@ namespace SW.Bitween.Resources.Accounts; [HandlerName("unlock")] -public class Unlock(BitweenDbContext dbContext, RequestContext requestContext) +public class Unlock(BitweenDbContext dbContext, RequestContext requestContext, SignInThrottle throttle) : ICommandHandler { public async Task Handle(int key, UnlockAccountModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Edit); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); account.Unlock(); await dbContext.SaveChangesAsync(); + // The per-address locks too. They are held per replica, so on a multi-replica deployment + // this lifts the ones on the replica that served it; the rest expire on their own. + throttle.ClearAccount(key); + return null; } } diff --git a/SW.Bitween.Api/Resources/Accounts/Update.cs b/SW.Bitween.Api/Resources/Accounts/Update.cs index 636e34c0..3be5412f 100644 --- a/SW.Bitween.Api/Resources/Accounts/Update.cs +++ b/SW.Bitween.Api/Resources/Accounts/Update.cs @@ -17,7 +17,7 @@ public async Task Handle(int key, UpdateAccountModel request) if (key != loggedInUserId) await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Edit); - var account = await dbContext.Set().FindAsync(key); + var account = await Members.FindAsync(dbContext, key); if (account is null) throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); @@ -28,6 +28,8 @@ public async Task Handle(int key, UpdateAccountModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Users.Edit); var role = (AccountRole)request.Role; + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + await GrantLimits.EnsureCallerHoldsRoles(dbContext, requestContext, [BuiltInRoleFor(role)]); account.SetRole(role); await AccountRoles.Set(dbContext, key, [BuiltInRoleFor(role)]); } diff --git a/SW.Bitween.Api/Resources/AdapterDrafts/Handlers.cs b/SW.Bitween.Api/Resources/AdapterDrafts/Handlers.cs new file mode 100644 index 00000000..16901613 --- /dev/null +++ b/SW.Bitween.Api/Resources/AdapterDrafts/Handlers.cs @@ -0,0 +1,178 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.AdapterDrafts; + +static class Drafts +{ + public static object Summary(AdapterDraft d) => new + { + d.Id, d.AdapterId, d.Language, d.Kind, d.BaseVersion, d.FilesHash, + d.CreatedOn, d.CreatedBy, d.ModifiedOn, d.ModifiedBy, + }; + + public static object Full(AdapterDraft d) => new + { + d.Id, d.AdapterId, d.Language, d.Kind, d.BaseVersion, d.FilesHash, + d.CreatedOn, d.CreatedBy, d.ModifiedOn, d.ModifiedBy, + d.Files, + }; + + public static async Task Find(BitweenDbContext db, int key) => + await db.Set().FindAsync(key) ?? throw new SWNotFoundException($"Adapter draft {key}"); +} + +/// Every draft in the editor, newest first. +public class Search(BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler +{ + public async Task Handle(AdapterDraftSearch request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + var drafts = await dbContext.Set().AsNoTracking() + .Where(d => request.AdapterId == null || d.AdapterId == request.AdapterId) + .OrderByDescending(d => d.ModifiedOn ?? d.CreatedOn) + .ToListAsync(); + return drafts.Select(Drafts.Summary); + } +} + +public class AdapterDraftSearch +{ + public string AdapterId { get; set; } +} + +public class Get(BitweenDbContext dbContext, RequestContext requestContext) : IGetHandler +{ + public async Task Handle(int key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + return Drafts.Full(await Drafts.Find(dbContext, key)); + } +} + +/// Starts a draft: a new adapter from the template, or a published version's source. +public class Create(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(AdapterDraftCreate request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + var draft = !string.IsNullOrWhiteSpace(request.FromAdapterId) + ? await workshop.FromVersionAsync(request.FromAdapterId.ToLowerInvariant(), request.FromVersion) + : await workshop.NewAsync(request.Name, request.Language, request.Kind ?? "handler", request.AdapterId); + dbContext.Add(draft); + await dbContext.SaveChangesAsync(); + return draft.Id; + } +} + +/// Saves the draft's files. +public class Update(BitweenDbContext dbContext, RequestContext requestContext) : ICommandHandler +{ + public async Task Handle(int key, AdapterDraftUpdate request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + var draft = await Drafts.Find(dbContext, key); + if (!string.IsNullOrEmpty(request.BaseHash) && request.BaseHash != draft.FilesHash) + throw new SWValidationException("DRAFT_CHANGED", + $"{draft.ModifiedBy ?? draft.CreatedBy ?? "Someone"} saved this draft at " + + $"{(draft.ModifiedOn ?? draft.CreatedOn):yyyy-MM-dd HH:mm} UTC, after you opened it. " + + "Saving now would replace their changes."); + draft.SetFiles(request.Files); + await dbContext.SaveChangesAsync(); + return Drafts.Summary(draft); + } +} + +public class Delete(BitweenDbContext dbContext, RequestContext requestContext) : IDeleteHandler +{ + public async Task Handle(int key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + dbContext.Remove(await Drafts.Find(dbContext, key)); + await dbContext.SaveChangesAsync(); + return null; + } +} + +/// Builds the draft and checks it against its contracts, as serverless test would. +[HandlerName("build")] +public class Build(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(int key, AdapterDraftRun request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + var draft = await Drafts.Find(dbContext, key); + var result = await workshop.BuildAsync(draft, request?.Settings, check: request?.BuildOnly != true); + return Describe(result); + } + + internal static object Describe(WorkshopBuild result) => new + { + result.Succeeded, + result.Conforms, + result.Problems, + result.Warnings, + result.Checks, + Settings = result.Manifest?.Properties.Select(p => new { p.Name, p.Description, p.Required, p.Secret, p.Default, p.Type }), + Commands = result.Manifest == null ? null : CommandsOf(result.Manifest), + result.Manifest?.Lifecycle, + result.Manifest?.Kinds, + }; + + // The commands a kind's contract names; the try panel offers these. + static string[] CommandsOf(Serverless.Contract.Catalog.AdapterManifest manifest) => + manifest.Kinds.SelectMany(k => k switch + { + "handler" or "mapper" => new[] { "Handle" }, + "validator" => ["Validate"], + "receiver" => ["Initialize", "ListFiles", "GetFile", "DeleteFile", "Finalize"], + _ => [], + }).Distinct().ToArray(); +} + +/// Builds the draft and calls one of its commands with the settings and input given. +[HandlerName("try")] +public class Try(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(int key, AdapterDraftRun request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Edit); + var draft = await Drafts.Find(dbContext, key); + return await workshop.TryAsync(draft, request?.Settings, request?.Command, request?.Input); + } +} + +/// +/// Publishes the draft as a new version, once it builds and passes its contract checks. Not made +/// current: what runs changes only when someone promotes it, or pins it on a subscription. +/// +[HandlerName("publish")] +public class Publish(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(int key, AdapterDraftPublish request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Operate); + var draft = await Drafts.Find(dbContext, key); + var who = requestContext.GetNameIdentifier(); + // The catalog says who published each version; a name reads better there than an id. + var account = int.TryParse(who, out var accountId) + ? await dbContext.Set().AsNoTracking().FirstOrDefaultAsync(a => a.Id == accountId) + : null; + var (version, build) = await workshop.PublishAsync(draft, request?.Version, request?.ReleaseNotes, request?.Settings, + account?.DisplayName ?? who ?? "Bitween"); + if (version == null) return new { Published = false, Build = Build.Describe(build) }; + + draft.Published(version); + dbContext.Add(new AdapterRelease(draft.AdapterId, version, AdapterRelease.PublishedAction, draft.Id, who)); + await dbContext.SaveChangesAsync(); + return new { Published = true, Version = version, Build = Build.Describe(build) }; + } +} diff --git a/SW.Bitween.Api/Resources/AdapterDrafts/Models.cs b/SW.Bitween.Api/Resources/AdapterDrafts/Models.cs new file mode 100644 index 00000000..05169ffe --- /dev/null +++ b/SW.Bitween.Api/Resources/AdapterDrafts/Models.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace SW.Bitween.Resources.AdapterDrafts; + +public class AdapterDraftCreate +{ + /// A new adapter: its name, language and kind, and optionally its id. + public string Name { get; set; } + public string Language { get; set; } + public string Kind { get; set; } + public string AdapterId { get; set; } + + /// Or a published version to start from: set both, and leave the rest. + public string FromAdapterId { get; set; } + public string FromVersion { get; set; } +} + +public class AdapterDraftUpdate +{ + public Dictionary Files { get; set; } + + /// + /// The draft's FilesHash when the editor opened it or last saved. A save over someone else's is + /// refused rather than silently discarding theirs; left out, the save goes through, as it + /// always did. + /// + public string BaseHash { get; set; } +} + +/// Settings to build, check or try the draft with. Used for the call and never stored. +public class AdapterDraftRun +{ + public Dictionary Settings { get; set; } + + /// Build only, without the contract checks. + public bool BuildOnly { get; set; } + + /// For try: the command, and its input as JSON or text. + public string Command { get; set; } + public string Input { get; set; } +} + +public class AdapterDraftPublish +{ + /// major, minor, patch, or an explicit version; patch when empty. + public string Version { get; set; } + public string ReleaseNotes { get; set; } + public Dictionary Settings { get; set; } +} diff --git a/SW.Bitween.Api/Resources/Adapters/AdapterListing.cs b/SW.Bitween.Api/Resources/Adapters/AdapterListing.cs index af0a9c67..47294c37 100644 --- a/SW.Bitween.Api/Resources/Adapters/AdapterListing.cs +++ b/SW.Bitween.Api/Resources/Adapters/AdapterListing.cs @@ -10,10 +10,17 @@ namespace SW.Bitween.Resources.Adapters; /// The adapter id, as a subscription stores it. /// In-process, so it has no published versions of its own. /// -/// Published version files, as paths relative to the remote adapter root — the trailing segment is +/// Published version files, as paths relative to the remote adapter root - the trailing segment is /// the version number. /// -public record AdapterEntry(string Key, bool Native, IReadOnlyList VersionPaths); +/// +/// What the adapter says about itself - the current package's manifest, or a native adapter's. Null +/// for a package published before manifests existed. +/// +/// The published adapter's catalog entry; null when it has none. +public record AdapterEntry(string Key, bool Native, IReadOnlyList VersionPaths, + SW.Serverless.Contract.Catalog.AdapterManifest Manifest = null, + SW.Serverless.Contract.Catalog.AdapterCatalogEntry Catalog = null); /// /// The list of adapters of one kind: the in-process ones plus whatever is published to storage. @@ -29,7 +36,9 @@ public class AdapterListing( ICloudFilesService cloudFilesService, NativeAdapterDiscoveryService nativeAdapterDiscovery, BitweenDbContext dbContext, - SW.Serverless.AdapterInstaller adapterInstaller) + SW.Serverless.AdapterInstaller adapterInstaller, + Services.Adapters.AdapterCatalog catalog, + Services.Adapters.NativeAdapterManifests nativeManifests) { /// The plural, lowercase kind: receivers, handlers, … /// Native adapters first, then the published ones. @@ -38,7 +47,7 @@ public async Task> List(string prefix) var index = serverlessOptions.AdapterRemotePath.Length + 1; var native = (await nativeAdapterDiscovery.GetNativeAdapters(prefix).ExceptRetiring(dbContext)) - .Select(key => new AdapterEntry(key, true, [])) + .Select(key => new AdapterEntry(key, true, [], nativeManifests.Get(key))) .ToList(); var files = (await ListByKindAsync(prefix)) @@ -59,7 +68,58 @@ public async Task> List(string prefix) .Select(v => v.Key[index..]) .ToList())); - return native.Concat(published).ToList(); + // The catalog adds what a package listing cannot: the current version, the history, and + // the presentation. An adapter without an entry is listed exactly as before. + var withCatalog = await Task.WhenAll(published.Select(async entry => + { + var found = await catalog.GetAsync(entry.Key); + return found == null ? entry : entry with { Manifest = found.Manifest, Catalog = found }; + })); + + var listed = withCatalog.Select(e => e.Key).ToHashSet(System.StringComparer.OrdinalIgnoreCase); + var catalogOnly = await CatalogOnlyAsync(prefix, listed); + + return native.Concat(withCatalog).Concat(catalogOnly).ToList(); + } + + /// + /// Adapters that are in the catalog but have no package at adapters/{id}, which is all + /// the listing above looks at. An adapter in another runtime than .NET is never put there - + /// older hosts would start it with dotnet - and neither is a .NET version published without + /// being made current. Each is listed under the kinds its manifests declare, or the old + /// naming convention, and only while it has a version that can still be used. + /// + private async Task> CatalogOnlyAsync(string prefix, ISet listed) + { + var kind = prefix?.TrimEnd('s') ?? ""; + var root = serverlessOptions.AdapterRemotePath; + var catalogRoot = SW.Serverless.Contract.Catalog.AdapterCatalogPaths.CatalogRoot(root); + + IEnumerable files; + try + { + files = await cloudFilesService.ListAsync(catalogRoot); + } + catch + { + // As everywhere the catalog is read: absent rather than fatal. + return []; + } + + var ids = files + .Where(f => f.Key.EndsWith(".json", System.StringComparison.OrdinalIgnoreCase)) + .Select(f => f.Key[catalogRoot.Length..^".json".Length]) + .Where(id => id.Length > 0 && !id.Contains('/') && !listed.Contains(id)) + .ToList(); + + var entries = await Task.WhenAll(ids.Select(id => catalog.GetAsync(id))); + return entries + .Where(e => e != null && e.Versions.Any(v => !v.Withdrawn)) + .Where(e => e.Id.StartsWith($"infolink6.{prefix}.", System.StringComparison.OrdinalIgnoreCase) || + (kind.Length > 0 && e.Versions.Append(new() { Manifest = e.Manifest }) + .Any(v => v.Manifest?.Kinds?.Contains(kind, System.StringComparer.OrdinalIgnoreCase) == true))) + .Select(e => new AdapterEntry(e.Id, false, [], e.Manifest ?? e.Versions.Last(v => !v.Withdrawn).Manifest, e)) + .ToList(); } /// @@ -69,7 +129,7 @@ public async Task> List(string prefix) /// Two ways an adapter says what it is for, and both are honoured. The Kind stamped on it at /// publish time is the real answer: it comes from the code rather than from whoever typed the /// id, it is the only way a third party's adapter can be found at all, and it lets an adapter - /// be reclassified without being renamed — a rename is not free, because every subscription + /// be reclassified without being renamed - a rename is not free, because every subscription /// stores the id. /// /// The infolink6.<kind>s. prefix is the old convention, and everything published before the @@ -83,7 +143,7 @@ private async Task> ListByKindAsync(string prefix) var byConvention = (await cloudFilesService.ListAsync($"{root}/infolink6.{prefix}")).ToList(); var named = byConvention.Select(i => i.Key).ToHashSet(System.StringComparer.OrdinalIgnoreCase); - // The plural the UI asks with — "handlers" — against the singular an adapter declares. + // The plural the UI asks with - "handlers" - against the singular an adapter declares. var kind = prefix?.TrimEnd('s') ?? ""; if (string.IsNullOrWhiteSpace(kind)) return byConvention; @@ -102,7 +162,7 @@ private async Task> ListByKindAsync(string prefix) /// /// The kinds one adapter declared. Metadata reads are cached by the installer, and an adapter /// whose metadata cannot be read declares nothing rather than taking the whole catalogue down - /// with it — the list is what an operator needs to configure anything at all. + /// with it - the list is what an operator needs to configure anything at all. /// private async Task DeclaredKindsAsync(string key, string root) { diff --git a/SW.Bitween.Api/Resources/Adapters/Catalog.cs b/SW.Bitween.Api/Resources/Adapters/Catalog.cs index 6f3d3fb9..a87fb39e 100644 --- a/SW.Bitween.Api/Resources/Adapters/Catalog.cs +++ b/SW.Bitween.Api/Resources/Adapters/Catalog.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.Resources.Adapters; /// answers with the adapters alone, which left a caller that needs to /// draw a form per adapter to ask once per row. On a screen listing /// all four kinds that is around ninety requests, six of which a browser will run at a time, so the -/// last of them waits behind fifteen rounds of queueing — and every one of those requests carries +/// last of them waits behind fifteen rounds of queueing - and every one of those requests carries /// its own permission check. Answering the whole kind at once makes it four requests for the screen. /// /// @@ -39,7 +39,7 @@ public async Task Handle(AdapterSearchRequest request) // Asked for all at once and left unthrottled on purpose. The native ones answer by // reflection and should not be made to queue, and how many published adapters may be - // running at a time is capped in ServerlessAdapterDescriber — process-wide, which is the + // running at a time is capped in ServerlessAdapterDescriber - process-wide, which is the // only place it can be, since this handler knows nothing of the other requests in flight. var described = await Task.WhenAll(adapters.Select(async a => (a.Key, Values: await Describe(a.Key)))); var byKey = described.ToDictionary(d => d.Key, d => d.Values); @@ -51,8 +51,49 @@ public async Task Handle(AdapterSearchRequest request) // Just the version numbers. VersionPaths carries each one as a path, which is what the // older shape passed through and what made a version read as an object rather than // "1.2.3" to anything trying to label it. - Versions = a.VersionPaths.Select(v => v.Split('/').Last()).ToList(), - StartupValues = byKey[a.Key] + // Old-layout version files, and the catalog's versions that can still be pinned - so the + // version label an older client shows is right for adapters published either way. + Versions = a.VersionPaths.Select(v => v.Split('/').Last()) + .Concat(a.Catalog?.Versions.Where(v => !v.Withdrawn).Select(v => v.Version) ?? []) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(v => System.Version.TryParse(v.Split('-')[0], out var parsed) ? parsed : new System.Version()) + .ToList(), + StartupValues = byKey[a.Key], + + // Additive: a client that predates the catalog ignores all of these. Every one is null + // or empty for an adapter published before manifests existed. + CurrentVersion = a.Catalog?.Current, + VersionHistory = a.Catalog?.Versions + .Select(v => new + { + v.Version, + v.PublishedOn, + v.PublishedBy, + v.Withdrawn, + v.Manifest?.ReleaseNotes, + MinBitweenVersion = v.Manifest?.Compatibility?.MinVersionOf(Services.Adapters.AdapterCatalog.Application), + // Whether the package carries its source, so a client offers to show it + // only where there is something to show. + HasSource = v.Manifest?.Source?.Files is { Count: > 0 }, + // Which the editor can open: Python and Node versions with their source. + v.Manifest?.Runtime + }) + .ToList(), + DisplayName = a.Manifest?.DisplayName, + a.Manifest?.Summary, + a.Manifest?.Description, + Publisher = a.Manifest?.Publisher?.Name, + PublisherUrl = a.Manifest?.Publisher?.Url, + a.Manifest?.License, + a.Manifest?.Homepage, + a.Manifest?.Repository, + Tags = a.Manifest?.Tags ?? [], + Categories = a.Manifest?.Categories ?? [], + a.Manifest?.Language, + a.Manifest?.Runtime, + a.Manifest?.Lifecycle, + Icon = a.Catalog?.IconDataUri, + MinBitweenVersion = a.Manifest?.Compatibility?.MinVersionOf(Services.Adapters.AdapterCatalog.Application) }); } @@ -64,7 +105,7 @@ private async Task> Describe(string adapterId) } catch (Exception) { - // One adapter that cannot be described — its runtime is missing locally, say — must not + // One adapter that cannot be described - its runtime is missing locally, say - must not // blank out the rest of the catalogue, including the native ones that resolved // perfectly well. It comes back with no properties, as it did when the caller was // asking row by row and swallowing the failure itself. diff --git a/SW.Bitween.Api/Resources/Adapters/GetProperties.cs b/SW.Bitween.Api/Resources/Adapters/GetProperties.cs index 8cc74b15..3056c347 100644 --- a/SW.Bitween.Api/Resources/Adapters/GetProperties.cs +++ b/SW.Bitween.Api/Resources/Adapters/GetProperties.cs @@ -37,9 +37,9 @@ async public Task Handle(string key) return _nativeAdapterDiscovery.GetExpectedStartupValues(decodedKey); } - // Handle serverless adapters. The native branch above returns a different shape — - // each key's default rather than a "key (default)" label — so it is left as it was. - var expected = AdapterStartupValues.WithoutSecretDefaults(await startupValues.Describe(decodedKey)); + // Handle serverless adapters. The native branch above returns a different shape - + // each key's default rather than a "key (default)" label - so it is left as it was. + var expected = AdapterStartupValues.WithoutSecretDefaults(await startupValues.DescribeOrNotFound(decodedKey)); return expected .ToList() diff --git a/SW.Bitween.Api/Resources/Adapters/GetStartupValues.cs b/SW.Bitween.Api/Resources/Adapters/GetStartupValues.cs index f8a120b1..a6f387b5 100644 --- a/SW.Bitween.Api/Resources/Adapters/GetStartupValues.cs +++ b/SW.Bitween.Api/Resources/Adapters/GetStartupValues.cs @@ -25,7 +25,7 @@ public async Task> Handle(string key) await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); return AdapterStartupValues.WithoutSecretDefaults( - await startupValues.Describe(System.Uri.UnescapeDataString(key))); + await startupValues.DescribeOrNotFound(System.Uri.UnescapeDataString(key))); } } } diff --git a/SW.Bitween.Api/Resources/Adapters/Promote.cs b/SW.Bitween.Api/Resources/Adapters/Promote.cs new file mode 100644 index 00000000..aa234027 --- /dev/null +++ b/SW.Bitween.Api/Resources/Adapters/Promote.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Adapters; + +public class AdapterPromoteRequest +{ + public string AdapterId { get; set; } + public string Version { get; set; } +} + +/// +/// Makes a published version the one that runs wherever nothing is pinned - rolling back is +/// promoting an older one. What serverless promote does, from Bitween, and in the audit trail. +/// +[HandlerName("promote")] +public class Promote(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(AdapterPromoteRequest request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Operate); + var adapterId = request.AdapterId?.ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(adapterId) || NativeAdapterDiscoveryService.IsNative(adapterId)) + throw new SWValidationException("AdapterId", "Only a published adapter has versions to promote."); + + await workshop.PromoteAsync(adapterId, request.Version); + dbContext.Add(new AdapterRelease(adapterId, request.Version, AdapterRelease.PromotedAction, null, requestContext.GetNameIdentifier())); + await dbContext.SaveChangesAsync(); + return new { AdapterId = adapterId, Current = request.Version }; + } +} diff --git a/SW.Bitween.Api/Resources/Adapters/RetiringMapper.cs b/SW.Bitween.Api/Resources/Adapters/RetiringMapper.cs index c6b45e10..470d3c1e 100644 --- a/SW.Bitween.Api/Resources/Adapters/RetiringMapper.cs +++ b/SW.Bitween.Api/Resources/Adapters/RetiringMapper.cs @@ -13,14 +13,14 @@ namespace SW.Bitween.Resources.Adapters; /// /// /// -/// This is how the old mapper retires. A subscription already using it is untouched — it keeps -/// running, and it keeps its own editor — but the mapper stops being offered as a choice once the +/// This is how the old mapper retires. A subscription already using it is untouched - it keeps +/// running, and it keeps its own editor - but the mapper stops being offered as a choice once the /// last subscription has moved across. The list of mappers then shrinks by itself as the migration /// finishes, rather than when someone remembers to delete the adapter. /// /// /// One-way on purpose: past that point there is no picking it again. Which is why the question of -/// what counts as "in use" is answered generously — an inactive subscription still holds its +/// what counts as "in use" is answered generously - an inactive subscription still holds its /// template and can be switched back on, so it counts. Withholding a mapper a subscription is /// still saved with would leave that subscription's own mapper missing from the list its picker /// draws from, which reads as no mapper chosen at all. @@ -43,7 +43,7 @@ internal static class RetiringMapper /// /// /// The database is only asked when the retiring mapper is in the list to begin with, so the - /// other adapter kinds — receivers, handlers, validators — cost nothing. + /// other adapter kinds - receivers, handlers, validators - cost nothing. /// public static async Task> ExceptRetiring( this IEnumerable adapters, BitweenDbContext dbContext) diff --git a/SW.Bitween.Api/Resources/Adapters/Source.cs b/SW.Bitween.Api/Resources/Adapters/Source.cs new file mode 100644 index 00000000..d7d76f8e --- /dev/null +++ b/SW.Bitween.Api/Resources/Adapters/Source.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Adapters; + +public class AdapterSourceRequest +{ + public string AdapterId { get; set; } + public string Version { get; set; } + + /// The file, relative to the source folder; for only. + public string Path { get; set; } +} + +/// +/// The files a published version's source holds, with their hashes, so a client can show the tree +/// and tell which files two versions differ in without reading any. +/// +[HandlerName("source")] +public class Source( + AdapterSourceReader reader, + BitweenDbContext dbContext, + RequestContext requestContext) : IQueryHandler +{ + public async Task Handle(AdapterSourceRequest request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.View); + + var listing = await reader.ListAsync(request.AdapterId?.ToLowerInvariant(), request.Version) + ?? throw new SWNotFoundException($"{request.AdapterId} {request.Version}"); + return listing; + } +} + +/// One source file of a published version, checked against its manifest's hash. Each read is audited. +[HandlerName("sourcefile")] +public class SourceFile( + AdapterSourceReader reader, + BitweenDbContext dbContext, + RequestContext requestContext) : IQueryHandler +{ + public async Task Handle(AdapterSourceRequest request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.View); + + var adapterId = request.AdapterId?.ToLowerInvariant(); + var file = await reader.ReadAsync(adapterId, request.Version, request.Path) + ?? throw new SWNotFoundException($"{request.AdapterId} {request.Version} {request.Path}"); + + dbContext.Add(new AdapterSourceAccess(adapterId, request.Version, request.Path, requestContext.GetNameIdentifier())); + await dbContext.SaveChangesAsync(); + return file; + } +} diff --git a/SW.Bitween.Api/Resources/Adapters/Versions.cs b/SW.Bitween.Api/Resources/Adapters/Versions.cs new file mode 100644 index 00000000..5affd34c --- /dev/null +++ b/SW.Bitween.Api/Resources/Adapters/Versions.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Adapters; + +public class AdapterVersionsRequest +{ + public string AdapterId { get; set; } +} + +/// Every published version of one adapter, with which is current: bitween adapter versions. +[HandlerName("versions")] +public class Versions(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : IQueryHandler +{ + public async Task Handle(AdapterVersionsRequest request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + if (string.IsNullOrWhiteSpace(request.AdapterId)) throw new SWValidationException("AdapterId", "Say which adapter."); + return await workshop.VersionsAsync(request.AdapterId.ToLowerInvariant()); + } +} + +/// Takes a published version out of use: bitween adapter withdraw. In the audit trail. +[HandlerName("withdraw")] +public class Withdraw(BitweenDbContext dbContext, RequestContext requestContext, AdapterWorkshop workshop) + : ICommandHandler +{ + public async Task Handle(AdapterPromoteRequest request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.AdapterSource.Operate); + var adapterId = request.AdapterId?.ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(adapterId) || NativeAdapterDiscoveryService.IsNative(adapterId)) + throw new SWValidationException("AdapterId", "Only a published adapter has versions to withdraw."); + + await workshop.WithdrawAsync(adapterId, request.Version); + dbContext.Add(new AdapterRelease(adapterId, request.Version, AdapterRelease.WithdrawnAction, null, requestContext.GetNameIdentifier())); + await dbContext.SaveChangesAsync(); + return new { AdapterId = adapterId, Withdrawn = request.Version }; + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs index c5156d49..6917cc0a 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs @@ -37,7 +37,7 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) if (model.NewIntegration != null) { - // Staged, not saved — the attachment takes its foreign key from the subscription EF + // Staged, not saved - the attachment takes its foreign key from the subscription EF // is tracking, so the pair lands on the one SaveChangesAsync below or not at all. // Nothing to check for a duplicate against: an integration that does not exist yet // cannot already be attached. @@ -74,7 +74,7 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) dbContext.Add(partnerLink); await dbContext.SaveChangesAsync(); // Attaching an existing integration changes nothing the cache holds, but staging a new - // one above creates a Subscription — and unconditional is what AddRoute does. + // one above creates a Subscription - and unconditional is what AddRoute does. await cache.BroadcastRevoke(); return null; diff --git a/SW.Bitween.Api/Resources/ApiGateways/Create.cs b/SW.Bitween.Api/Resources/ApiGateways/Create.cs index 6758b6f1..45f8e91c 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Create.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Create.cs @@ -19,7 +19,8 @@ public async Task Handle(ApiGatewayCreate model) { Name = model.Name, UrlName = model.UrlName, - Inactive = model.Inactive + Inactive = model.Inactive, + FailureAnswer = model.FailureAnswer ?? GatewayFailureAnswer.Accurate, }; if (model.Authentication != null) GatewayAuthentication.Apply(entity, model.Authentication); diff --git a/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs index 0250fb0c..4b91b215 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Resources.ApiGateways; /// -/// The url name is the path partners call — /api/gateway/{urlName}/sync — so anything +/// The url name is the path partners call - /api/gateway/{urlName}/sync - so anything /// needing escaping there makes a gateway that reads as configured and cannot be reached. A /// space is the one that actually happens: it saves, the endpoint shown on the page is the one /// the partner copies, and the call 404s with nothing on screen to explain it. @@ -30,7 +30,7 @@ public static void Validate(string urlName) if (!Allowed().IsMatch(urlName)) throw new SWValidationException("GATEWAY_URL_NAME_INVALID", $"'{urlName}' cannot be used in a URL. Use lowercase letters, digits, hyphens " + - "and underscores, with / between parts — no spaces, and no part starting or " + + "and underscores, with / between parts - no spaces, and no part starting or " + "ending with a separator."); // The call ends in /sync or /async, and it is the last segment that says which. A name @@ -38,7 +38,7 @@ public static void Validate(string urlName) var last = urlName[(urlName.LastIndexOf('/') + 1)..]; if (last is "sync" or "async") throw new SWValidationException("GATEWAY_URL_NAME_INVALID", - $"'{urlName}' cannot end in '{last}' — partners add /sync or /async after it."); + $"'{urlName}' cannot end in '{last}' - partners add /sync or /async after it."); } /// @@ -46,7 +46,7 @@ public static void Validate(string urlName) /// updating, so saving a gateway without touching its url name isn't a collision with itself. /// /// - /// The column is uniquely indexed, so this was already refused — as a constraint violation + /// The column is uniquely indexed, so this was already refused - as a constraint violation /// that reached the screen as "Request failed (500)", with nothing to say the name was taken /// or which gateway has it. /// diff --git a/SW.Bitween.Api/Resources/ApiGateways/Get.cs b/SW.Bitween.Api/Resources/ApiGateways/Get.cs index 81a7e6d6..d486cbc9 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Get.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Get.cs @@ -31,6 +31,7 @@ public async Task Handle(int key) Name = gateway.Name, UrlName = gateway.UrlName, Inactive = gateway.Inactive, + FailureAnswer = gateway.FailureAnswer, Authentication = GatewayAuthentication.Read(gateway), DefaultKeyHeader = options.PartnerKeyHeader, PartnersCount = gateway.Partners.Count, diff --git a/SW.Bitween.Api/Resources/ApiGateways/Rejections.cs b/SW.Bitween.Api/Resources/ApiGateways/Rejections.cs new file mode 100644 index 00000000..e00a93e7 --- /dev/null +++ b/SW.Bitween.Api/Resources/ApiGateways/Rejections.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Gateway; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ApiGateways; + +/// +/// The calls to this gateway turned away, and why: an unknown or missing key, a partner not +/// attached, the gateway switched off, no subscription behind it, too many calls. Kept in memory on +/// the node that answers, since it started (see ); another node keeps +/// its own. +/// +[HandlerName("rejections")] +public class Rejections(BitweenDbContext dbContext, RequestContext requestContext, GatewayActivity activity) + : IGetHandler +{ + public async Task Handle(int key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ApiGateways.View); + var urlName = await dbContext.Set().AsNoTracking().Where(g => g.Id == key) + .Select(g => g.UrlName).SingleOrDefaultAsync() ?? throw new SWNotFoundException(key.ToString()); + + var (counts, recent) = activity.RefusalsOf(urlName); + + // Every node's, kept past restarts: the hourly metrics count each refusal under the reason. + var scopeKey = urlName.Trim('/').ToLowerInvariant(); + var weekAgo = DateTime.UtcNow.AddDays(-7); + var lastSevenDays = await dbContext.Set().AsNoTracking() + .Where(e => e.Scope == Domain.Metrics.ErrorStats.GatewayScope && e.ScopeKey == scopeKey && e.Hour >= weekAgo) + .GroupBy(e => e.Sample) + .Select(g => new { Reason = g.Key, Count = g.Sum(x => (long)x.Count) }) + .ToDictionaryAsync(x => x.Reason ?? "", x => x.Count); + + return new + { + Node = Services.Cluster.NodeHeartbeat.NodeName, + activity.Since, + Counts = counts, + LastSevenDays = lastSevenDays, + Recent = recent.Select(r => new { r.On, r.Reason, r.Status, r.Address }), + }; + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/Search.cs b/SW.Bitween.Api/Resources/ApiGateways/Search.cs index ee384c82..d2c7ebf2 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Search.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Search.cs @@ -24,6 +24,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa Name = gateway.Name, UrlName = gateway.UrlName, Inactive = gateway.Inactive, + FailureAnswer = gateway.FailureAnswer, PartnersCount = gateway.Partners.Count }; diff --git a/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs index 50b46212..c67d0dab 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Resources.ApiGateways { /// - /// Paged, searched view of one gateway's attachments — the full list lives on + /// Paged, searched view of one gateway's attachments - the full list lives on /// for callers that need every attached partner id (e.g. the /// attach-partner picker's exclude list), this is only for the gateway page's own table. /// diff --git a/SW.Bitween.Api/Resources/ApiGateways/Update.cs b/SW.Bitween.Api/Resources/ApiGateways/Update.cs index b7a3ada1..cc855af7 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Update.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Update.cs @@ -28,6 +28,7 @@ public async Task Handle(int key, ApiGatewayUpdate model) entity.Name = model.Name; entity.UrlName = model.UrlName; entity.Inactive = model.Inactive; + if (model.FailureAnswer != null) entity.FailureAnswer = model.FailureAnswer.Value; if (model.Authentication != null) GatewayAuthentication.Apply(entity, model.Authentication); diff --git a/SW.Bitween.Api/Resources/Audit/Search.cs b/SW.Bitween.Api/Resources/Audit/Search.cs index 55f021b4..69da9cff 100644 --- a/SW.Bitween.Api/Resources/Audit/Search.cs +++ b/SW.Bitween.Api/Resources/Audit/Search.cs @@ -12,14 +12,14 @@ namespace SW.Bitween.Resources.Audit; /// -/// The audit trail, newest first. Serves both the global history page and the per-entity timeline — +/// The audit trail, newest first. Serves both the global history page and the per-entity timeline - /// the latter is this same query with and /// set, which is what the composite index covers. /// public class Search(BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler { - /// Rows per page ceiling — each row carries its changes as JSON. + /// Rows per page ceiling - each row carries its changes as JSON. const int MaxLimit = 200; public async Task Handle(SearchAuditModel request) diff --git a/SW.Bitween.Api/Resources/BitweenDocs/Get.cs b/SW.Bitween.Api/Resources/BitweenDocs/Get.cs index 8b655910..1c3495a8 100644 --- a/SW.Bitween.Api/Resources/BitweenDocs/Get.cs +++ b/SW.Bitween.Api/Resources/BitweenDocs/Get.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.Resources.BitweenDocs; /// /// An exchange file's content, for the exchange drawer. Signed-in members with the exchanges view -/// only, and only keys that are exactly one of an exchange's files — it used to read any key in the +/// only, and only keys that are exactly one of an exchange's files - it used to read any key in the /// bucket for anyone. /// public class Get(BitweenDbContext dbContext, RequestContext requestContext, XchangeService xchangeService) diff --git a/SW.Bitween.Api/Resources/BusGateways/Create.cs b/SW.Bitween.Api/Resources/BusGateways/Create.cs index 938de652..cfe99cd1 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Create.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Create.cs @@ -42,7 +42,7 @@ public async Task Handle(BusGatewayCreate model) /// /// Shared by Create and Update. An endpoint is what the supervisor turns into the adapter's /// consume list, so an external gateway without one is a gateway that can never receive - /// anything — and it would fail silently, which is the worst way for it to fail. + /// anything - and it would fail silently, which is the worst way for it to fail. /// internal static async Task EnsureDataSourceAsync(BitweenDbContext dbContext, BusGatewayCreate model) { @@ -56,8 +56,8 @@ internal static async Task EnsureDataSourceAsync(BitweenDbContext dbContext, Bus if (dataSource == null) throw new SWNotFoundException($"DataSource with Id {model.DataSourceId} not found"); - // A data source is not only a broker connection — a resident adapter holding a database - // session is one too — and only a broker has queues to consume. Reading this from the + // A data source is not only a broker connection - a resident adapter holding a database + // session is one too - and only a broker has queues to consume. Reading this from the // kind rather than from the adapter id keeps the rule true for providers nobody has // written yet. if (dataSource.Kind != Domain.DataSources.DataSourceKind.Broker) @@ -67,7 +67,7 @@ internal static async Task EnsureDataSourceAsync(BitweenDbContext dbContext, Bus if (string.IsNullOrWhiteSpace(model.Endpoint)) throw new SWException( - "An external bus gateway needs an endpoint — the queue, topic or subscription " + "An external bus gateway needs an endpoint - the queue, topic or subscription " + "on that data source it reads from."); // Two gateways consuming one endpoint on one data source would both be offered every diff --git a/SW.Bitween.Api/Resources/BusGateways/Search.cs b/SW.Bitween.Api/Resources/BusGateways/Search.cs index a2de4774..df12c923 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Search.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Search.cs @@ -32,7 +32,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa .Select(d => d.Name).FirstOrDefault(), // Null name means the internal bus, which is what the list column - // reads — an operator should be able to tell at a glance which of + // reads - an operator should be able to tell at a glance which of // their gateways reach outside. DataSourceId = gateway.DataSourceId, DataSourceName = dataSources.Where(d => d.Id == gateway.DataSourceId) diff --git a/SW.Bitween.Api/Resources/BusGateways/Update.cs b/SW.Bitween.Api/Resources/BusGateways/Update.cs index 1dcbd5b6..8fd31e5d 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Update.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Update.cs @@ -24,7 +24,7 @@ public async Task Handle(int key, BusGatewayUpdate model) await EnsureEndpointFreeAsync(_dbContext, key, model); - // The bound document is still fixed at creation — routes' subscriptions belong to it — + // The bound document is still fixed at creation - routes' subscriptions belong to it - // but where the messages COME from is exactly the thing an operator needs to change // without rebuilding the gateway and all of its routes. entity.Name = model.Name; @@ -51,8 +51,8 @@ private static async Task EnsureEndpointFreeAsync( if (dataSource == null) throw new SWNotFoundException($"DataSource with Id {model.DataSourceId} not found"); - // A data source is not only a broker connection — a resident adapter holding a database - // session is one too — and only a broker has queues to consume. Reading this from the + // A data source is not only a broker connection - a resident adapter holding a database + // session is one too - and only a broker has queues to consume. Reading this from the // kind rather than from the adapter id keeps the rule true for providers nobody has // written yet. if (dataSource.Kind != Domain.DataSources.DataSourceKind.Broker) @@ -62,7 +62,7 @@ private static async Task EnsureEndpointFreeAsync( if (string.IsNullOrWhiteSpace(model.Endpoint)) throw new SWException( - "An external bus gateway needs an endpoint — the queue, topic or subscription " + "An external bus gateway needs an endpoint - the queue, topic or subscription " + "on that data source it reads from."); var endpointTaken = await dbContext.Set() diff --git a/SW.Bitween.Api/Resources/Cluster/Nodes.cs b/SW.Bitween.Api/Resources/Cluster/Nodes.cs new file mode 100644 index 00000000..623b605b --- /dev/null +++ b/SW.Bitween.Api/Resources/Cluster/Nodes.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Cluster; +using SW.Bitween.Domain.DataSources; +using SW.Bitween.Services.Cluster; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Cluster; + +/// +/// The nodes of this Bitween, as each last described itself (see ): +/// whether it is still there, whether it runs data sources, which adapter runtimes it has, and the +/// leases and exclusive data sources it holds. +/// +[HandlerName("nodes")] +public class Nodes(BitweenDbContext dbContext, RequestContext requestContext) : IQueryHandler +{ + public async Task Handle() + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Settings.View); + + var now = DateTime.UtcNow; + var nodes = await dbContext.Set().AsNoTracking().OrderBy(n => n.Id).ToListAsync(); + var leases = await dbContext.Set().AsNoTracking().ToListAsync(); + var held = await dbContext.Set().AsNoTracking() + .Where(d => d.OwnedByNode != null) + .Select(d => new { d.Id, d.Name, d.OwnedByNode }) + .ToListAsync(); + + return new + { + AnsweredBy = NodeHeartbeat.NodeName, + GoneAfterSeconds = (int)NodeHeartbeat.GoneAfter.TotalSeconds, + Nodes = nodes.Select(n => new + { + Name = n.Id, + n.Host, + n.StartedOn, + n.LastSeenOn, + Online = now - n.LastSeenOn <= NodeHeartbeat.GoneAfter, + n.Version, + n.DataSources, + Runtimes = (n.Runtimes ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries), + Leases = leases.Where(l => l.OwnerNode == n.Id).Select(l => new { Resource = l.Id, l.Term, l.AcquiredOn }), + // Recorded as " (term N)" by the supervisor that took the source. + HoldsDataSources = held.Where(d => d.OwnedByNode == n.Id || d.OwnedByNode.StartsWith(n.Id + " ")) + .Select(d => new { d.Id, d.Name }), + }), + }; + } +} diff --git a/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs b/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs index 62ba0cbd..d59ad8b9 100644 --- a/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs +++ b/SW.Bitween.Api/Resources/Dashboard/RetrySummary.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.Resources.Dashboard; /// anything at all. /// /// -/// Both in one handler because both belong to the same panel — a repeat offender means little +/// Both in one handler because both belong to the same panel - a repeat offender means little /// without knowing whether retries generally work here. Nine attempts against the same connection /// error is a configuration problem someone has to fix; nine attempts where most retries do /// eventually succeed is bad luck. @@ -65,8 +65,8 @@ group result by result.Success && !result.ResponseBad into worked var succeeded = retries.Where(r => r.Worked).Sum(r => r.Count); var finished = retries.Sum(r => r.Count); - // Chains that keep failing: a failure nothing has been retried from — so it is where that - // chain has got to — which is itself a retry, so the chain is at least two attempts long. + // Chains that keep failing: a failure nothing has been retried from - so it is where that + // chain has got to - which is itself a retry, so the chain is at least two attempts long. var candidates = await ( from xchange in _dbContext.Set() join result in _dbContext.Set() on xchange.Id equals result.Id @@ -125,7 +125,7 @@ orderby xchange.StartedOn descending /// How many attempts each chain has taken, counting the original as one. /// /// - /// A level of every chain per query rather than a walk per chain — five hundred chains four + /// A level of every chain per query rather than a walk per chain - five hundred chains four /// attempts deep is four queries, not two thousand. XchangeResult.AttemptNumber holds /// this already, but only for failures a retry policy's group matched, so a chain retried by /// hand has none and it cannot be read from there. diff --git a/SW.Bitween.Api/Resources/DataSourceStatements/Create.cs b/SW.Bitween.Api/Resources/DataSourceStatements/Create.cs index 06ac374c..bcb3ca55 100644 --- a/SW.Bitween.Api/Resources/DataSourceStatements/Create.cs +++ b/SW.Bitween.Api/Resources/DataSourceStatements/Create.cs @@ -34,7 +34,7 @@ await requestContext.EnsurePermission(dbContext, throw new SWNotFoundException($"DataSource with id '{dataSourceId}' was not found"); // Only a relational source runs SQL. Refused rather than stored, because a statement on a - // broker is configuration nothing will ever read — and silently keeping it is how people + // broker is configuration nothing will ever read - and silently keeping it is how people // conclude the feature is broken. if (dataSource.Kind != DataSourceKind.Relational) throw new SWException( @@ -66,7 +66,7 @@ await requestContext.EnsurePermission(dbContext, /// /// Refuses SQL the database itself will not accept, while the person who wrote it is still - /// looking at it. The check prepares the statement — parsed and planned, never run. + /// looking at it. The check prepares the statement - parsed and planned, never run. /// /// Silent when the adapter cannot answer: a connection that is down is a fact about the /// connection, and blocking someone from saving a fix because the thing they are fixing it @@ -85,7 +85,7 @@ internal static async Task EnsureSqlIsValid( /// /// Case-insensitively unique within the data source. The database index enforces uniqueness but - /// its collation is provider-specific, so the case rule is applied here — where it can also say + /// its collation is provider-specific, so the case rule is applied here - where it can also say /// which statement it collided with, rather than surfacing as a constraint violation. /// internal static async Task EnsureNameIsFree(BitweenDbContext dbContext, int dataSourceId, diff --git a/SW.Bitween.Api/Resources/DataSourceStatements/Delete.cs b/SW.Bitween.Api/Resources/DataSourceStatements/Delete.cs index 79d04b63..3f9ea096 100644 --- a/SW.Bitween.Api/Resources/DataSourceStatements/Delete.cs +++ b/SW.Bitween.Api/Resources/DataSourceStatements/Delete.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.DataSourceStatements; /// /// Refused while anything names it, and the refusal lists what. A statement deleted out from under -/// a live subscription does not fail at delete time — it fails on the next message, as "not a +/// a live subscription does not fail at delete time - it fails on the next message, as "not a /// statement this data source defines", somewhere nobody is watching. /// public class Delete(BitweenDbContext dbContext, RequestContext requestContext, diff --git a/SW.Bitween.Api/Resources/DataSourceStatements/Search.cs b/SW.Bitween.Api/Resources/DataSourceStatements/Search.cs index 36b057d3..8a09bed9 100644 --- a/SW.Bitween.Api/Resources/DataSourceStatements/Search.cs +++ b/SW.Bitween.Api/Resources/DataSourceStatements/Search.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.Resources.DataSourceStatements; /// Every statement, with the count that matters: how many subscriptions name it. /// /// Zero is the whole point. Without it, SQL nobody uses accumulates forever because nobody can -/// prove it is safe to remove — which is exactly what happened while these lived in one JSON blob. +/// prove it is safe to remove - which is exactly what happened while these lived in one JSON blob. /// public class Search(BitweenDbContext dbContext, RequestContext requestContext, StatementUsageReader usage) : ISearchyHandler diff --git a/SW.Bitween.Api/Resources/DataSourceStatements/Update.cs b/SW.Bitween.Api/Resources/DataSourceStatements/Update.cs index 403da43b..9fff3292 100644 --- a/SW.Bitween.Api/Resources/DataSourceStatements/Update.cs +++ b/SW.Bitween.Api/Resources/DataSourceStatements/Update.cs @@ -23,7 +23,7 @@ public async Task Handle(int key, DataSourceStatementUpdate model) await Create.EnsureNameIsFree(dbContext, entity.DataSourceId, model.Name, existingId: key); // A rename breaks every subscription naming the old one, and there is no way to fix that - // from here — the subscription's properties are its own. So it is refused while anything + // from here - the subscription's properties are its own. So it is refused while anything // still points at it, and the message says what to change first. var renaming = !string.Equals(entity.Name, model.Name?.Trim(), System.StringComparison.OrdinalIgnoreCase); @@ -41,7 +41,7 @@ public async Task Handle(int key, DataSourceStatementUpdate model) } // Only when it actually changed: re-checking untouched SQL would refuse a rename, or a - // change of owner, because of a table someone dropped last week — a fault worth surfacing + // change of owner, because of a table someone dropped last week - a fault worth surfacing // but not here, and not as a block on an unrelated edit. // True when the SQL did not change: there was nothing to check, which is not the same as // "could not check" and should not warn as though it were. diff --git a/SW.Bitween.Api/Resources/DataSourceStatements/Usage.cs b/SW.Bitween.Api/Resources/DataSourceStatements/Usage.cs index ec1af7fd..1e24ebc1 100644 --- a/SW.Bitween.Api/Resources/DataSourceStatements/Usage.cs +++ b/SW.Bitween.Api/Resources/DataSourceStatements/Usage.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.DataSourceStatements; /// -/// Not just how many, but WHICH — with the slot and the operation, so "can I change this?" is +/// Not just how many, but WHICH - with the slot and the operation, so "can I change this?" is /// answerable without opening every subscription bound to the connection. /// [HandlerName("usage")] diff --git a/SW.Bitween.Api/Resources/DataSources/Create.cs b/SW.Bitween.Api/Resources/DataSources/Create.cs index 13d7acfb..dbe00995 100644 --- a/SW.Bitween.Api/Resources/DataSources/Create.cs +++ b/SW.Bitween.Api/Resources/DataSources/Create.cs @@ -23,8 +23,8 @@ public async Task Handle(DataSourceCreate model) if (nameTaken) throw new SWException($"A data source named '{model.Name}' already exists."); - // Nothing is stored behind a sentinel on a create, so any that arrives — from a data source - // copied out of Get, say — is dropped rather than saved as the literal string. + // Nothing is stored behind a sentinel on a create, so any that arrives - from a data source + // copied out of Get, say - is dropped rather than saved as the literal string. var properties = Secrets.Merge(null, model.Properties); var entity = new DataSource @@ -50,8 +50,8 @@ public async Task Handle(DataSourceCreate model) /// /// Enforced here rather than only in the validator, because the validator runs in the HTTP - /// pipeline and nothing else does: a caller reaching the handler another way — the supervisor's - /// own tests, a future internal caller — would otherwise save a combination that can never + /// pipeline and nothing else does: a caller reaching the handler another way - the supervisor's + /// own tests, a future internal caller - would otherwise save a combination that can never /// work. A soft ceiling above the hard one is unreachable: the runtime fails the allocation /// first, so the graceful recycle it was configured for never happens. /// @@ -64,7 +64,7 @@ internal static void EnsureCeilingsAreUsable(int softMb, int hardMb, if (cpuPercent < 0 || cpuSamples < 0) throw new SWException("A CPU ceiling cannot be negative. Use 0 for the host default."); - // Above 100 is not a ceiling, because the figure is a share of the whole node — nothing can + // Above 100 is not a ceiling, because the figure is a share of the whole node - nothing can // ever exceed it, so the limit would silently never fire. if (cpuPercent > 100) throw new SWException( @@ -99,7 +99,7 @@ public Validate() RuleFor(i => i.Name).NotEmpty().MaximumLength(200); RuleFor(i => i.AdapterId).NotEmpty().MaximumLength(200); - // Zero is meaningful — it turns deduplication off — so only a negative is rejected. + // Zero is meaningful - it turns deduplication off - so only a negative is rejected. RuleFor(i => i.DeduplicationWindowDays).GreaterThanOrEqualTo(0); // Zero means "leave the host default alone" for both. diff --git a/SW.Bitween.Api/Resources/DataSources/Delete.cs b/SW.Bitween.Api/Resources/DataSources/Delete.cs index 03cb3022..3bffa6c6 100644 --- a/SW.Bitween.Api/Resources/DataSources/Delete.cs +++ b/SW.Bitween.Api/Resources/DataSources/Delete.cs @@ -15,7 +15,7 @@ public async Task Handle(int key) { await requestContext.EnsurePermission(dbContext, Model.Permissions.DataSources.Delete); - // The database refuses this anyway — the foreign key restricts — but a raw constraint + // The database refuses this anyway - the foreign key restricts - but a raw constraint // violation tells an operator nothing about which gateway is in the way. var gateways = await dbContext.Set() .Where(gateway => gateway.DataSourceId == key) @@ -29,7 +29,7 @@ public async Task Handle(int key) string.Join(", ", gateways) + ". Point them at the internal bus, or delete them, first."); - // Subscription.DataSourceId restricts too, and was the one left to the database — the + // Subscription.DataSourceId restricts too, and was the one left to the database - the // commoner case of the two, since a data source is normally read by an integration long // before any gateway feeds off it. var subscriptions = await dbContext.Set() diff --git a/SW.Bitween.Api/Resources/DataSources/Inspect.cs b/SW.Bitween.Api/Resources/DataSources/Inspect.cs index d46b4956..ce9ce174 100644 --- a/SW.Bitween.Api/Resources/DataSources/Inspect.cs +++ b/SW.Bitween.Api/Resources/DataSources/Inspect.cs @@ -11,13 +11,13 @@ namespace SW.Bitween.Resources.DataSources; /// -/// Asks the RUNNING adapter what it can see right now — the broker's topology, or the +/// Asks the RUNNING adapter what it can see right now - the broker's topology, or the /// database's catalog. /// /// Deliberately not the same shape as . A test starts a throwaway instance, /// because its whole job is to answer "would these settings work" before anything depends on them. /// Discover and GetStats are questions about the connection that is actually serving traffic, so -/// they go to that instance — starting a second one would answer about a connection nobody is +/// they go to that instance - starting a second one would answer about a connection nobody is /// using, and on a broker that authorises per-connection it might not even answer the same way. /// /// Both are read-only: neither consumes, acknowledges or publishes anything. @@ -33,7 +33,7 @@ public class Inspect(BitweenDbContext dbContext, RequestContext requestContext, /// /// A database adapter widens this: Describe says what the engine and this login can do, and /// Discover walks the catalog. Both are read-only. Query, Execute, Call, Batch and BulkLoad are - /// deliberately absent and must stay absent — a View-level read must not become a way to run + /// deliberately absent and must stay absent - a View-level read must not become a way to run /// SQL against a customer's database by naming it in a request body. private static readonly string[] Allowed = ["Discover", "GetStats", "Describe"]; @@ -65,10 +65,10 @@ public async Task Handle(int key, DataSourceInspectRequest request) // opposite reactions. An exclusive broker connection is held by one node, so this // is the normal answer everywhere else and nothing is wrong. A pooled database // connection is held by every node that runs work, so "not here" means it is not - // running at all — which is a fault worth chasing. + // running at all - which is a fault worth chasing. Error = kind == DataSourceKind.Relational ? "This node is not running the adapter for this data source. A connection " - + "pool is held by every node, so this means it has not started — check the " + + "pool is held by every node, so this means it has not started - check the " + "live connection panel for why." : "This node is not running the adapter for this data source, so it has " + "nothing to ask. A broker connection is exclusive: only the node holding " diff --git a/SW.Bitween.Api/Resources/DataSources/Search.cs b/SW.Bitween.Api/Resources/DataSources/Search.cs index d9ed2365..d01f8b69 100644 --- a/SW.Bitween.Api/Resources/DataSources/Search.cs +++ b/SW.Bitween.Api/Resources/DataSources/Search.cs @@ -50,7 +50,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa query = query.AsNoTracking(); // Properties are deliberately absent from the list: it is a table of connections, and - // sending every credential — even masked — to render a row is not worth the exposure. + // sending every credential - even masked - to render a row is not worth the exposure. if (lookup) return await query.Search(searchyRequest.Conditions) diff --git a/SW.Bitween.Api/Resources/DataSources/Secrets.cs b/SW.Bitween.Api/Resources/DataSources/Secrets.cs index c7208fa1..0e2a2b61 100644 --- a/SW.Bitween.Api/Resources/DataSources/Secrets.cs +++ b/SW.Bitween.Api/Resources/DataSources/Secrets.cs @@ -9,8 +9,8 @@ namespace SW.Bitween.Resources.DataSources; /// /// This does NOT ask the adapter which of its startup values are marked secure, the way /// does for subscription properties. Describing an adapter -/// means starting it, and a bus provider is resident — started once and held for the life of the -/// node — so asking would either start a second copy of a connection that is meant to be exclusive +/// means starting it, and a bus provider is resident - started once and held for the life of the +/// node - so asking would either start a second copy of a connection that is meant to be exclusive /// or block on one that is already running. The data source names its own secrets instead, which /// also lets an operator protect a field the adapter author never thought to mark. /// @@ -35,7 +35,7 @@ public static bool IsSecret(string name, IEnumerable declared) => /// /// A copy with every secret replaced. An empty value is left alone, so "not set" stays - /// distinguishable from "set but hidden" — the difference between a form that is finished and + /// distinguishable from "set but hidden" - the difference between a form that is finished and /// one that is not. /// public static Dictionary Mask( @@ -52,7 +52,7 @@ public static Dictionary Mask( /// /// Resolves sentinels against what is stored. A sentinel with nothing behind it is dropped - /// rather than saved literally — otherwise a data source copied from a response would + /// rather than saved literally - otherwise a data source copied from a response would /// authenticate with the string "__private__". /// public static Dictionary Merge( @@ -73,7 +73,7 @@ public static Dictionary Merge( /// /// The secret names to record. Whatever the caller declared, plus anything matching a - /// well-known credential name — so the list stays true even when the form did not tick it. + /// well-known credential name - so the list stays true even when the form did not tick it. /// public static List Declare( IReadOnlyDictionary properties, IEnumerable declared) diff --git a/SW.Bitween.Api/Resources/DataSources/Telemetry.cs b/SW.Bitween.Api/Resources/DataSources/Telemetry.cs index fbc71434..b8697e69 100644 --- a/SW.Bitween.Api/Resources/DataSources/Telemetry.cs +++ b/SW.Bitween.Api/Resources/DataSources/Telemetry.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.Resources.DataSources; /// /// The data source row carries a summary written back on the reconcile loop, which is up to thirty /// seconds old and deliberately small. This reads the heartbeat directly, so an operator watching a -/// queue drain sees it drain — and gets the two things the row has never carried: the per-queue +/// queue drain sees it drain - and gets the two things the row has never carried: the per-queue /// depths the adapter can see, and the host-observed process figures that keep working when the /// adapter is wedged and reporting nothing at all. /// @@ -47,8 +47,8 @@ public async Task Handle(int key) LastHeartbeatOn = dataSource.LastHeartbeatOn, }; - // Registered only when BusProvidersEnabled, so a node with the feature off answers - // "not here" rather than failing to resolve a service. + // A node with BusProvidersEnabled off runs no data source, so it finds no instance and + // answers "not here". var instance = adapters?.Describe() .FirstOrDefault(h => h.InstanceKey == key.ToString()); diff --git a/SW.Bitween.Api/Resources/DataSources/Test.cs b/SW.Bitween.Api/Resources/DataSources/Test.cs index 4f303918..f7835f33 100644 --- a/SW.Bitween.Api/Resources/DataSources/Test.cs +++ b/SW.Bitween.Api/Resources/DataSources/Test.cs @@ -27,20 +27,20 @@ namespace SW.Bitween.Resources.DataSources; /// never drained, and it is stopped again before this returns. /// [HandlerName("test")] -public class Test(BitweenDbContext dbContext, RequestContext requestContext, +public class Test(BitweenDbContext dbContext, RequestContext requestContext, BitweenOptions bitweenOptions, IResidentAdapterHost adapters = null) : ICommandHandler { public async Task Handle(int key, DataSourceTestRequest request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.DataSources.Operate); - // Registered only when BusProvidersEnabled, so say which switch is off rather than - // failing to resolve a service the operator has never heard of. The switch is named for - // brokers because it predates database sources, so the message says what it now gates - // rather than repeating a name that means nothing to someone configuring PostgreSQL. - if (adapters == null) + // The host is registered on every node - Python and Node adapters run on it - but data + // sources connect only where BusProvidersEnabled turns them on, so say which switch is off. + // It is named for brokers because it predates database sources, so the message says what + // it now gates rather than repeating a name that means nothing to someone configuring PostgreSQL. + if (adapters == null || !bitweenOptions.BusProvidersEnabled) return Failed("Resident data source providers are turned off on this node, so nothing " - + "can connect from here. Turn on Bitween:BusProvidersEnabled — the " + + "can connect from here. Turn on Bitween:BusProvidersEnabled - the " + "switch is older than database sources and still carries the bus name."); var dataSource = await dbContext.Set().AsNoTracking() @@ -65,7 +65,7 @@ public async Task Handle(int key, DataSourceTestRequest request) startupValues["Consume"] = "false"; // Composed here as well as in the supervisor, because statements are rows rather than a - // property on the data source — and without this the test quietly stopped covering them. + // property on the data source - and without this the test quietly stopped covering them. // Preparing every statement against the live schema is most of what the button is FOR: it // is where a typo or a dropped column is caught, and a test that silently checks nothing // still reports success. @@ -100,7 +100,7 @@ public async Task Handle(int key, DataSourceTestRequest request) // A broker that cannot be reached throws on start, before any stage runs. That is a // result, not a server error: the operator asked whether it works, and it does not. // - // What Bitween observes in that case is only "Adapter stream closed" — true, and no + // What Bitween observes in that case is only "Adapter stream closed" - true, and no // use at all. The adapter printed the actual reason before it died, so that is what // this answers with; without it, the one control meant to save reading a log is the // control that sends you to read one. diff --git a/SW.Bitween.Api/Resources/DataSources/Update.cs b/SW.Bitween.Api/Resources/DataSources/Update.cs index 0c38bcea..7dac3d40 100644 --- a/SW.Bitween.Api/Resources/DataSources/Update.cs +++ b/SW.Bitween.Api/Resources/DataSources/Update.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using FluentValidation; using Microsoft.EntityFrameworkCore; @@ -28,12 +29,17 @@ public async Task Handle(int key, DataSourceUpdate model) // Secrets came out of Get masked, so put them back from what is stored. A form that only // changed the prefetch must not overwrite the password with a row of dots. - var properties = Secrets.Merge(entity.Properties, model.Properties); + var keep = AdapterSecretProperties.MayKeepStoredSecrets(entity.AdapterId, model.AdapterId, + entity.Properties, model.Properties ?? new Dictionary()); + var properties = Secrets.Merge(keep ? entity.Properties : null, model.Properties); entity.Name = model.Name; entity.AdapterId = model.AdapterId; entity.Kind = Create.ParseKind(model.Kind); - entity.Placement = Create.ParsePlacement(model.Placement); + // Left out means unchanged. It used to mean Auto, so every client that didn't send it, + // the admin UI included, put an Exclusive or PerNode source back to Auto on any save. + if (!string.IsNullOrWhiteSpace(model.Placement)) + entity.Placement = Create.ParsePlacement(model.Placement); entity.Properties = properties; entity.SecretProperties = Secrets.Declare(properties, model.SecretProperties); entity.Inactive = model.Inactive; diff --git a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs index 4bb54d94..2e3d325a 100644 --- a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs +++ b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs @@ -12,7 +12,7 @@ public class RunNow(BitweenDbContext dbContext, RequestContext requestContext, { public async Task Handle(string key, DelayedRetryRunNow request) { - // What's being operated on is an exchange, not the policy that scheduled the retry — + // What's being operated on is an exchange, not the policy that scheduled the retry - // and this is the same page the UI gates on exchange permissions. await requestContext.EnsurePermission(dbContext, Model.Permissions.Exchanges.Operate); @@ -34,6 +34,7 @@ public async Task Handle(string key, DelayedRetryRunNow request) : "This retry could not be carried out: the exchange it belonged to no longer exists."); } + dbContext.Record(requestContext, OperatorAction.RunRetryNow, key); await dbContext.SaveChangesAsync(); return null; } diff --git a/SW.Bitween.Api/Resources/DelayedRetries/Search.cs b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs index 80996549..f5686b5a 100644 --- a/SW.Bitween.Api/Resources/DelayedRetries/Search.cs +++ b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs @@ -21,7 +21,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa join xchange in dbContext.Set() on delayedRetry.Id equals xchange.Id join result in dbContext.Set() on xchange.Id equals result.Id into xr from result in xr.DefaultIfEmpty() - // Same left-join Xchanges/Search.cs uses — the UI lists a pending retry by + // Same left-join Xchanges/Search.cs uses - the UI lists a pending retry by // what it carries, not by its id, so the properties have to come along. join promoted in dbContext.Set() on xchange.Id equals promoted.Id into xp from promoted in xp.DefaultIfEmpty() diff --git a/SW.Bitween.Api/Resources/Documents/BusMessageTypeNames.cs b/SW.Bitween.Api/Resources/Documents/BusMessageTypeNames.cs index ee7c020c..2b06f91e 100644 --- a/SW.Bitween.Api/Resources/Documents/BusMessageTypeNames.cs +++ b/SW.Bitween.Api/Resources/Documents/BusMessageTypeNames.cs @@ -15,7 +15,7 @@ public static class BusMessageTypeNames /// Refuses a name another type holds. Compared lower-cased, because that is how the bus /// compares them: both BasicPublisher and ConsumerDefinition derive the routing key with /// ToLower(), so "Foo" and "foo" are one message on the wire. ToLower() rather than a - /// provider-specific collation — this runs on Postgres, MySql and MsSql. + /// provider-specific collation - this runs on Postgres, MySql and MsSql. /// /// A type paused on the bus holds its name too: its queue is still collecting messages for /// it, and a second type on the same name would consume them. Said as such, because diff --git a/SW.Bitween.Api/Resources/Documents/Create.cs b/SW.Bitween.Api/Resources/Documents/Create.cs index 29e470ed..46c5c0a3 100644 --- a/SW.Bitween.Api/Resources/Documents/Create.cs +++ b/SW.Bitween.Api/Resources/Documents/Create.cs @@ -20,7 +20,7 @@ public async Task Handle(DocumentCreate model) await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.Create); // Same check Update makes, and compared the same way. Without it two types - // could be created under one name, and then neither could be saved again — + // could be created under one name, and then neither could be saved again - // Update refuses the name it already has. Ignoring case, because a list // holding both "Invoice" and "invoice" reads as a mistake, not a choice. var wantedName = (model.Name ?? string.Empty).ToLower(); @@ -38,6 +38,7 @@ public async Task Handle(DocumentCreate model) await BusMessageTypeNames.EnsureFree(dbContext, model.BusMessageTypeName); PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); + await DocumentSchema.EnsureUsable(model.DocumentFormat, model.ValidationSchema); var entity = new Document(code, model.Name, model.DocumentFormat) { @@ -49,6 +50,7 @@ public async Task Handle(DocumentCreate model) if (model.PromotedProperties != null) entity.SetDictionaries(model.PromotedProperties.ToDictionary()); + entity.SetValidationSchema(model.ValidationSchema); dbContext.Add(entity); await dbContext.SaveChangesAsync(); // Routing resolves an information type by name off the cache, so a new one is diff --git a/SW.Bitween.Api/Resources/Documents/Delete.cs b/SW.Bitween.Api/Resources/Documents/Delete.cs index 0154a41c..037fc9c9 100644 --- a/SW.Bitween.Api/Resources/Documents/Delete.cs +++ b/SW.Bitween.Api/Resources/Documents/Delete.cs @@ -21,11 +21,11 @@ async public Task Handle(int key) await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.Delete); // Seeded by the model and referenced by every aggregation, so it is not the user's - // to remove — the same footing as the system partner. + // to remove - the same footing as the system partner. if (key == Document.AggregationDocumentId) throw new SWException("The built-in Aggregation Document can't be deleted."); - // Configuration pointing at this type is a real block — a subscription or a bus + // Configuration pointing at this type is a real block - a subscription or a bus // gateway left behind would name a type that no longer exists. Said plainly, because // the database says it as a foreign-key violation, which reached the screen as a bare // 500 naming a constraint. @@ -42,13 +42,13 @@ async public Task Handle(int key) // Exchanges are the deliberate exception. They are this type's history rather than // configuration depending on it, and history should not be able to strand a type - // nobody uses any more — so they go with it. + // nobody uses any more - so they go with it. await using var transaction = await dbContext.Database.BeginTransactionAsync(); var xchangeIds = dbContext.Set().Where(x => x.DocumentId == key).Select(x => x.Id); // Scheduled retries are keyed by exchange id but have no relationship configured, so - // nothing clears them on their own — they would be left pointing at exchanges that no + // nothing clears them on their own - they would be left pointing at exchanges that no // longer exist. Results, aggregations and promoted properties all cascade from the // exchange itself and need no help here. await dbContext.Set().Where(d => xchangeIds.Contains(d.Id)).ExecuteDeleteAsync(); diff --git a/SW.Bitween.Api/Resources/Documents/Get.cs b/SW.Bitween.Api/Resources/Documents/Get.cs index be295311..b1ed5f3d 100644 --- a/SW.Bitween.Api/Resources/Documents/Get.cs +++ b/SW.Bitween.Api/Resources/Documents/Get.cs @@ -21,7 +21,7 @@ public async Task Handle(int key) await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); // DocumentRow rather than DocumentUpdate: it is the read shape, and RetiredOn belongs - // on it rather than on the model used to write a type — retiring is its own command, + // on it rather than on the model used to write a type - retiring is its own command, // not a field an update can set. return await dbContext.Set().Search("Id", key).Select(document => new DocumentRow { @@ -32,6 +32,7 @@ public async Task Handle(int key) BusMessageTypeName = document.BusMessageTypeName, DuplicateInterval = document.DuplicateInterval, PromotedProperties = document.PromotedProperties.ToKeyAndValueCollection(), + ValidationSchema = document.ValidationSchema, DocumentFormat = document.DocumentFormat, DisregardsUnfilteredMessages = document.DisregardsUnfilteredMessages ?? false, RetiredOn = document.RetiredOn, diff --git a/SW.Bitween.Api/Resources/Documents/GetProperties.cs b/SW.Bitween.Api/Resources/Documents/GetProperties.cs index 13659dcb..ab9086a9 100644 --- a/SW.Bitween.Api/Resources/Documents/GetProperties.cs +++ b/SW.Bitween.Api/Resources/Documents/GetProperties.cs @@ -18,7 +18,7 @@ async public Task Handle(int key) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Documents.View); - var document = await dbContext.FindAsync(key); + var document = await dbContext.FindAsync(key) ?? throw new SWNotFoundException(key.ToString()); return document.PromotedProperties.ToDictionary(k => k.Key, v => v.Key); } } diff --git a/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs index 1463c74e..c692f138 100644 --- a/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs +++ b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs @@ -44,7 +44,7 @@ public static void Check(ICollection promotedProperties, DocumentFo // // The '$' branch is checked rather than trusted. Treating any leading '$' as // proof of a JSONPath let "$", "$." and "$.." through, none of which select - // anything — they saved cleanly and failed only once a message arrived. A step + // anything - they saved cleanly and failed only once a message arrived. A step // is a '.'/'..' followed by a name, or a bracket; names stay deliberately // permissive (anything but a delimiter) so paths that already work keep working. var valid = trimmed.StartsWith("$") diff --git a/SW.Bitween.Api/Resources/Documents/Retire.cs b/SW.Bitween.Api/Resources/Documents/Retire.cs index 9c0624b0..083b4078 100644 --- a/SW.Bitween.Api/Resources/Documents/Retire.cs +++ b/SW.Bitween.Api/Resources/Documents/Retire.cs @@ -34,7 +34,7 @@ public async Task Handle(int key, DocumentRetire request) if (entity.RetiredOn == null) { // Configuration still pointing at it would be left running against a type the UI - // no longer offers — which reads as the type having been removed when it has not. + // no longer offers - which reads as the type having been removed when it has not. // Said before retiring rather than after, so nothing half-happens. if (await dbContext.Set().AnyAsync(s => s.DocumentId == key)) throw new SWException( @@ -53,7 +53,7 @@ public async Task Handle(int key, DocumentRetire request) await dbContext.SaveChangesAsync(); // The receiving path resolves types through the cache, so a retired type would keep - // being offered — and keep accepting work — for the rest of the cache's window. + // being offered - and keep accepting work - for the rest of the cache's window. await cache.BroadcastRevoke(); return new { entity.Id, entity.RetiredOn }; diff --git a/SW.Bitween.Api/Resources/Documents/Search.cs b/SW.Bitween.Api/Resources/Documents/Search.cs index 335e9018..29a75b55 100644 --- a/SW.Bitween.Api/Resources/Documents/Search.cs +++ b/SW.Bitween.Api/Resources/Documents/Search.cs @@ -34,6 +34,7 @@ async public Task Handle(SearchyRequest searchyRequest, bool lookup = fa BusEnabled = document.BusEnabled, DuplicateInterval = document.DuplicateInterval, PromotedProperties = document.PromotedProperties.ToKeyAndValueCollection(), + ValidationSchema = document.ValidationSchema, DocumentFormat = document.DocumentFormat, RetiredOn = document.RetiredOn, // A correlated count, so the "used by" column the UI shows costs one diff --git a/SW.Bitween.Api/Resources/Documents/Update.cs b/SW.Bitween.Api/Resources/Documents/Update.cs index fd63cdb5..55dba241 100644 --- a/SW.Bitween.Api/Resources/Documents/Update.cs +++ b/SW.Bitween.Api/Resources/Documents/Update.cs @@ -55,24 +55,29 @@ public async Task Handle(int key, DocumentUpdate model) await BusMessageTypeNames.EnsureFree(dbContext, model.BusMessageTypeName, key); PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); + // Checked against the format being saved, so switching a type from JSON to XML can't + // leave a JSON Schema on it. + var schema = model.ValidationSchema ?? entity.ValidationSchema; + await DocumentSchema.EnsureUsable(model.DocumentFormat, schema); var oldBusMessageTypeName = entity.BusMessageTypeName; // An absent list means none, the same as it does for retry policy groups. - // Left implicit it threw ArgumentNullException — a 500 for a request the + // Left implicit it threw ArgumentNullException - a 500 for a request the // API had simply never decided the meaning of. entity.SetDictionaries((model.PromotedProperties ?? []).ToDictionary()); - // Name/Code have private setters — SetProperties only writes public-setter + // Name/Code have private setters - SetProperties only writes public-setter // properties, so it silently no-ops on these two (verified empirically). entity.SetName(model.Name); entity.SetCode(code); // The route key is what identifies the type; the body carries an Id too, and // SetProperties copies it straight onto the tracked entity. A caller that omits it - // sends 0, which EF rejects as an attempt to change a primary key — a 500 for a + // sends 0, which EF rejects as an attempt to change a primary key - a 500 for a // request that was perfectly well formed. Normalising it here makes the copy a no-op // whatever the body said. model.Id = key; dbContext.Entry(entity).SetProperties(model); + entity.SetValidationSchema(schema); await dbContext.SaveChangesAsync(); await BitweenCache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs index 937d473d..4ab1347d 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Search.cs @@ -35,7 +35,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa var result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync(); - // The keys stay — the reference picker offers {{globals.set.key}} from this list, and a + // The keys stay - the reference picker offers {{globals.set.key}} from this list, and a // secret you cannot point at is no use to anyone. Only the values behind them go. foreach (var row in result) row.Values = AdapterSecretProperties.Mask(row.Values, row.SecretProperties); diff --git a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs index 1ae74cee..f87dd985 100644 --- a/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs +++ b/SW.Bitween.Api/Resources/GlobalAdapterValuesSets/Update.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentValidation; @@ -21,7 +22,9 @@ public async Task Handle(string key, GlobalAdapterValuesSetUpdate reques entity.Name = request.Name; // A secret the form did not touch comes back as the sentinel; restore it from storage // rather than saving the mask. - entity.Values = AdapterSecretProperties.Merge(entity.Values, request.Values); + entity.Values = AdapterSecretProperties.Merge(entity.Values, request.Values, + AdapterSecretProperties.MayKeepStoredSecrets(null, null, entity.Values, + request.Values ?? new Dictionary())); entity.SecretProperties = request.SecretProperties?.ToList() ?? []; await dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Mappers/Preview.cs b/SW.Bitween.Api/Resources/Mappers/Preview.cs index baaa51a7..1f0cd91f 100644 --- a/SW.Bitween.Api/Resources/Mappers/Preview.cs +++ b/SW.Bitween.Api/Resources/Mappers/Preview.cs @@ -45,8 +45,10 @@ public async Task Handle(MapperPreviewRequest request) var jObj = parsed as JObject; var jArr = parsed as JArray; + // Declared secrets are masked: the template is the caller's own, so it can print any + // value it is given. JObject? partnerObj = partner?.AdapterProperties?.Count > 0 - ? JObject.FromObject(partner.AdapterProperties) + ? JObject.FromObject(AdapterSecretProperties.Mask(partner.AdapterProperties, partner.SecretProperties)) : null; JObject? globalsObj = null; @@ -55,7 +57,7 @@ public async Task Handle(MapperPreviewRequest request) { globalsObj = new JObject(); foreach (var set in nonEmptySets) - globalsObj[set.Id] = JObject.FromObject(set.Values); + globalsObj[set.Id] = JObject.FromObject(AdapterSecretProperties.Mask(set.Values, set.SecretProperties)); } if (jObj != null && (partnerObj != null || globalsObj != null)) diff --git a/SW.Bitween.Api/Resources/MappingPreviews/Preview.cs b/SW.Bitween.Api/Resources/MappingPreviews/Preview.cs index 4d34e577..20bdf7f2 100644 --- a/SW.Bitween.Api/Resources/MappingPreviews/Preview.cs +++ b/SW.Bitween.Api/Resources/MappingPreviews/Preview.cs @@ -35,7 +35,7 @@ public class MappingPreviewResponse /// public List RuleErrors { get; set; } = new(); - /// Set when the mapping could not be attempted at all — unreadable document or rules. + /// Set when the mapping could not be attempted at all - unreadable document or rules. public string? Error { get; set; } } @@ -50,7 +50,7 @@ public class MappingPreviewError /// /// /// -/// Runs the same three steps the exchange pipeline runs — read, map, write — against the same +/// Runs the same three steps the exchange pipeline runs - read, map, write - against the same /// context factory. That is deliberate and it is the point: the old preview generated its own /// template and assembled its own partner values, and the two drifted, so a mapping that previewed /// correctly could fail in production. @@ -60,11 +60,17 @@ public class MappingPreviewError /// yet. /// /// -public class Preview(MappingContextFactory contextFactory) +public class Preview(RequestContext requestContext, BitweenDbContext dbContext, + MappingContextFactory contextFactory) : ICommandHandler { public async Task Handle(MappingPreviewRequest request) { + // The editor opens while creating a subscription as well as while editing one, so either + // right will do. Without a guard any signed-in token could map any partner's values. + await requestContext.EnsurePermission(dbContext, + Model.Permissions.Subscriptions.Create, Model.Permissions.Subscriptions.Edit); + MappingRules rules; try { @@ -99,7 +105,7 @@ public async Task Handle(MappingPreviewRequest request) return new MappingPreviewResponse { Error = ex.Message }; } - var context = await contextFactory.Build(request.PartnerId); + var context = await contextFactory.Build(request.PartnerId, maskSecrets: true); try { diff --git a/SW.Bitween.Api/Resources/Metrics/AdapterDetail.cs b/SW.Bitween.Api/Resources/Metrics/AdapterDetail.cs new file mode 100644 index 00000000..00361706 --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/AdapterDetail.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// +/// One adapter's use in detail: by version, by node, by role and runtime, by day; its most common +/// errors; and the subscriptions that use it with what they moved through it. +/// +[HandlerName("adapter")] +public class AdapterDetail(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) + : IQueryHandler +{ + public async Task Handle(AdapterMetricsRequest request) + { + await requestContext.EnsurePermission(db, Model.Permissions.Subscriptions.View); + if (string.IsNullOrWhiteSpace(request?.Id)) throw new SWValidationException("Id", "Say which adapter."); + var id = request.Id.Trim().ToLowerInvariant(); + var days = MetricsQueries.Days(request.Days); + return await cache.Cached($"adapter.{id}.{days}", () => Compute(id, days)); + } + + async Task Compute(string id, int days) + { + var to = MetricsRecorder.HourOf(DateTime.UtcNow).AddHours(1); + var from = to.AddDays(-days); + var rows = MetricsQueries.Adapters(db, from, to).Where(s => s.AdapterId == id); + + var total = (await rows.GroupBy(_ => 1).SumBy()).FirstOrDefault() ?? new AdapterTotals(); + var versions = await rows.GroupBy(s => s.Version).SumBy(); + var nodes = await rows.GroupBy(s => s.Node).SumBy(); + var roles = await rows.GroupBy(s => new { s.Role, s.Runtime }).SumBy(); + var daily = await rows.GroupBy(s => s.Hour.Date).SumBy(); + + var errorRows = db.Set().AsNoTracking() + .Where(e => e.Scope == ErrorStats.AdapterScope && e.ScopeKey == id && e.Hour >= from && e.Hour < to); + var errors = await errorRows.GroupBy(e => e.Fingerprint) + .Select(g => new { Fingerprint = g.Key, Count = g.Sum(x => (long)x.Count), LastSeenOn = g.Max(x => x.LastSeenOn) }) + .OrderByDescending(e => e.Count).Take(10).ToListAsync(); + var fingerprints = errors.Select(e => e.Fingerprint).ToList(); + var samples = (await errorRows.Where(e => fingerprints.Contains(e.Fingerprint)) + .Select(e => new { e.Fingerprint, e.Sample, e.LastSeenOn }).ToListAsync()) + .GroupBy(e => e.Fingerprint).ToDictionary(g => g.Key, g => g.OrderByDescending(e => e.LastSeenOn).First().Sample); + + // The subscriptions configured with it now, and what went through them in the window. + var users = await db.Set().AsNoTracking() + .Where(s => s.HandlerId.ToLower() == id || s.MapperId.ToLower() == id || + s.ReceiverId.ToLower() == id || s.ValidatorId.ToLower() == id) + .Select(s => new { s.Id, s.Name, s.Type, s.HandlerVersion, s.MapperVersion }) + .ToListAsync(); + var userIds = users.Select(u => u.Id).ToList(); + var through = (await MetricsQueries.Exchanges(db, from, to).Where(s => userIds.Contains(s.SubscriptionId)) + .GroupBy(s => s.SubscriptionId).SumBy()).ToDictionary(t => t.Key); + + return new + { + Id = id, + Days = days, + Since = await MetricsQueries.AdapterSinceAsync(db), + Total = Shape(total), + Versions = versions.OrderByDescending(v => v.Calls).Select(v => new { Version = v.Key, Totals = Shape(v) }), + Nodes = nodes.OrderByDescending(n => n.Calls).Select(n => new { Node = n.Key, Totals = Shape(n) }), + Roles = roles.Select(r => new { r.Key.Role, r.Key.Runtime, Totals = Shape(r) }), + Daily = daily.OrderBy(d => d.Key).Select(d => new { Day = MetricsQueries.Utc(d.Key), d.Calls, d.Failures, d.P95Ms }), + Errors = errors.Select(e => new + { + e.Fingerprint, e.Count, LastSeenOn = MetricsQueries.Utc(e.LastSeenOn), Sample = samples.GetValueOrDefault(e.Fingerprint), + }), + Subscriptions = users.Select(u => + { + var t = through.GetValueOrDefault(u.Id); + return new + { + SubscriptionId = u.Id, u.Name, Type = u.Type.ToString(), + Exchanges = t?.Total ?? 0, Failed = t?.Failed ?? 0, Bytes = (t?.BytesIn ?? 0) + (t?.BytesOut ?? 0), + }; + }).OrderByDescending(u => u.Exchanges), + }; + } + + static object Shape(AdapterTotals t) => new + { + t.Calls, t.Failures, t.FailureRate, t.Timeouts, t.LimitStops, t.Crashes, t.P50Ms, t.P95Ms, MaxMs = t.DurationMaxMs, + }; +} diff --git a/SW.Bitween.Api/Resources/Metrics/Adapters.cs b/SW.Bitween.Api/Resources/Metrics/Adapters.cs new file mode 100644 index 00000000..10434631 --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Adapters.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// Every adapter's use over the window: calls, failures by kind, timings, calls by day, last call. +[HandlerName("adapters")] +public class Adapters(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) + : IQueryHandler +{ + public async Task Handle(MetricsWindowRequest request) + { + await requestContext.EnsurePermission(db, Model.Permissions.Subscriptions.View); + var days = MetricsQueries.Days(request?.Days); + return await cache.Cached($"adapters.{days}", () => Compute(days)); + } + + async Task Compute(int days) + { + var to = MetricsRecorder.HourOf(DateTime.UtcNow).AddHours(1); + var from = to.AddDays(-days); + var totals = await MetricsQueries.Adapters(db, from, to).GroupBy(s => s.AdapterId).SumBy(); + var last = await MetricsQueries.Adapters(db, to.AddDays(-30), to) + .GroupBy(s => s.AdapterId).Select(g => new { g.Key, Last = g.Max(x => x.Hour) }) + .ToDictionaryAsync(x => x.Key, x => MetricsQueries.Utc(x.Last)); + var dailyRows = await MetricsQueries.Adapters(db, from, to) + .GroupBy(s => new { s.AdapterId, Day = s.Hour.Date }) + .Select(g => new { g.Key.AdapterId, g.Key.Day, Calls = g.Sum(x => (long)x.Calls), Failures = g.Sum(x => (long)x.Failures) }) + .ToListAsync(); + var firstDay = from.Date; + + return new + { + Since = await MetricsQueries.AdapterSinceAsync(db), + Days = days, + Adapters = totals.Select(t => new + { + Id = t.Key, t.Calls, t.Failures, t.FailureRate, t.Timeouts, t.LimitStops, t.Crashes, t.P50Ms, t.P95Ms, + MaxMs = t.DurationMaxMs, + LastCalledOn = last.TryGetValue(t.Key, out var l) ? l : (DateTime?)null, + Daily = Enumerable.Range(0, days).Select(i => + { + var day = firstDay.AddDays(i); + var r = dailyRows.Where(d => d.AdapterId == t.Key && d.Day.Date == day).ToList(); + return new { Day = MetricsQueries.Utc(day), Calls = r.Sum(x => x.Calls), Failures = r.Sum(x => x.Failures) }; + }), + }), + // Adapters with no calls in the window but some in the last 30 days, for "last called". + Quiet = last.Where(l => totals.All(t => t.Key != l.Key)).Select(l => new { Id = l.Key, LastCalledOn = l.Value }), + }; + } +} diff --git a/SW.Bitween.Api/Resources/Metrics/Insights.cs b/SW.Bitween.Api/Resources/Metrics/Insights.cs new file mode 100644 index 00000000..9f7c6b24 --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Insights.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.Services.Adapters; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// +/// Housekeeping: configuration that is switched off, unused or out of date, each with a link to +/// it. Nothing here is wrong as such; it is what to look at when tidying up an instance. +/// +[HandlerName("insights")] +public class Insights(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache, AdapterCatalog catalog) + : IQueryHandler +{ + const int Listed = 10; + + public record Item(string Label, string Href); + + public record Insight(string Kind, string Severity, string Title, string Detail, int Count, IEnumerable Items); + + public async Task Handle() + { + await requestContext.EnsurePermission(db, Model.Permissions.Dashboard.View); + return await cache.Cached("insights", Compute); + } + + async Task Compute() + { + var to = MetricsRecorder.HourOf(DateTime.UtcNow).AddHours(1); + var since = await MetricsQueries.SinceAsync(db); + var insights = new List(); + + var subscriptions = await db.Set().AsNoTracking() + .Select(s => new + { + s.Id, s.Name, s.Inactive, s.PausedOn, s.PausedAutomatically, s.RetryPolicyId, s.WorkGroupId, s.PartnerId, + s.HandlerId, s.HandlerVersion, s.MapperId, s.MapperVersion, s.QuietSince, + }).ToListAsync(); + Item Link(int id, string name) => new(name, $"/subscriptions/{id}"); + + // Set by QuietSubscriptionsJob: expected traffic that hasn't come. Longest quiet first. + var quiet = subscriptions.Where(s => !s.Inactive && s.PausedOn == null && s.QuietSince != null) + .OrderBy(s => s.QuietSince).ToList(); + if (quiet.Count > 0) + insights.Add(new("quiet-subscriptions", "warn", "Subscriptions gone quiet", + "Expected traffic hasn't arrived. Check the partner or the source they read from.", + quiet.Count, quiet.Take(Listed).Select(s => new Item($"{s.Name} (since {s.QuietSince:yyyy-MM-dd HH:mm} UTC)", + $"/subscriptions/{s.Id}")))); + + var paused = subscriptions.Where(s => !s.Inactive && s.PausedOn != null).ToList(); + if (paused.Count > 0) + insights.Add(new("paused", "warn", "Paused subscriptions", + $"{paused.Count(s => s.PausedAutomatically)} paused themselves after repeated failures.", + paused.Count, paused.Take(Listed).Select(s => Link(s.Id, s.Name)))); + + var inactive = subscriptions.Where(s => s.Inactive).ToList(); + if (inactive.Count > 0) + insights.Add(new("inactive", "info", "Switched-off subscriptions", "Kept, but not running.", + inactive.Count, inactive.Take(Listed).Select(s => Link(s.Id, s.Name)))); + + // Quiet for 30 days - only once the metrics reach that far back. + if (since != null && since <= to.AddDays(-30)) + { + var active = (await MetricsQueries.Exchanges(db, to.AddDays(-30), to).Where(s => s.Total > 0) + .Select(s => s.SubscriptionId).Distinct().ToListAsync()).ToHashSet(); + var unused = subscriptions.Where(s => !s.Inactive && s.PausedOn == null && !active.Contains(s.Id)).ToList(); + if (unused.Count > 0) + insights.Add(new("unused-subscriptions", "info", "Subscriptions without traffic for 30 days", + "Running, but nothing has gone through them.", unused.Count, unused.Take(Listed).Select(s => Link(s.Id, s.Name)))); + } + + // Pinned to a version that has since been withdrawn: it keeps running, but nobody can pin it again. + var withdrawn = new List(); + foreach (var s in subscriptions.Where(s => s.HandlerVersion != null || s.MapperVersion != null)) + foreach (var (id, version) in new[] { (s.HandlerId, s.HandlerVersion), (s.MapperId, s.MapperVersion) }) + if (id != null && version != null && (await catalog.GetAsync(id))?.Find(version)?.Withdrawn == true) + withdrawn.Add(new($"{s.Name} (pins {id} v{version})", $"/subscriptions/{s.Id}")); + if (withdrawn.Count > 0) + insights.Add(new("withdrawn-pins", "warn", "Pinned to a withdrawn version", + "Still runs it, but the version is withdrawn. Move it to a current version.", withdrawn.Count, withdrawn.Take(Listed))); + + var usedPolicies = subscriptions.Where(s => s.RetryPolicyId != null).Select(s => s.RetryPolicyId.Value).ToHashSet(); + var policies = await db.Set().AsNoTracking().Select(p => new { p.Id, p.Name }).ToListAsync(); + var unusedPolicies = policies.Where(p => !usedPolicies.Contains(p.Id)).ToList(); + if (unusedPolicies.Count > 0) + insights.Add(new("unused-retry-policies", "info", "Retry policies nothing uses", "No subscription is assigned them.", + unusedPolicies.Count, unusedPolicies.Take(Listed).Select(p => new Item(p.Name, $"/retry-policies/{p.Id}")))); + + var usedGroups = subscriptions.Where(s => s.WorkGroupId != null).Select(s => s.WorkGroupId.Value).ToHashSet(); + var groups = await db.Set().AsNoTracking().Select(g => new { g.Id, g.Name }).ToListAsync(); + var unusedGroups = groups.Where(g => !usedGroups.Contains(g.Id)).ToList(); + if (unusedGroups.Count > 0) + insights.Add(new("unused-work-groups", "info", "Work groups nothing uses", "Their queues are kept for no subscription.", + unusedGroups.Count, unusedGroups.Take(Listed).Select(g => new Item(g.Name, $"/work-groups/{g.Id}")))); + + var linked = subscriptions.Where(s => s.PartnerId != null).Select(s => s.PartnerId.Value) + .Concat(await db.Set().AsNoTracking().Select(a => a.PartnerId).ToListAsync()) + .Concat(await db.Set().AsNoTracking().Where(r => r.PartnerId != null).Select(r => r.PartnerId.Value).ToListAsync()) + .ToHashSet(); + var partners = await db.Set().AsNoTracking().Where(p => p.Id != Partner.SystemId) + .Select(p => new { p.Id, p.Name }).ToListAsync(); + var loose = partners.Where(p => !linked.Contains(p.Id)).ToList(); + if (loose.Count > 0) + insights.Add(new("unlinked-partners", "info", "Partners not connected to anything", + "Not attached to a gateway, a route or a subscription.", loose.Count, + loose.Take(Listed).Select(p => new Item(p.Name, $"/partners/{p.Id}")))); + + // Whatever signs in with one of these stops working the day it runs out, so it's worth knowing + // two weeks ahead. Only a token still in use counts; one already replaced is expected to lapse. + var now = DateTime.UtcNow; + var soon = now.AddDays(14); + var expiring = await (from t in db.Set().AsNoTracking() + join a in db.Set().AsNoTracking() on t.AccountId equals a.Id + where t.RevokedOn == null && t.ExpiresOn > now && t.ExpiresOn <= soon && !a.Disabled + orderby t.ExpiresOn + select new { a.Id, Account = a.DisplayName, t.Name, t.ExpiresOn }) + .ToListAsync(); + if (expiring.Count > 0) + insights.Add(new("expiring-service-tokens", "warn", "Service tokens expiring within two weeks", + "Whatever uses them stops signing in when they do. Issue a replacement, then revoke the old one.", + expiring.Count, + expiring.Take(Listed).Select(t => new Item($"{t.Account}: {t.Name} ({t.ExpiresOn:yyyy-MM-dd})", + $"/team/service-accounts/{t.Id}")))); + + var changedSettings = await db.Set().AsNoTracking().CountAsync(); + if (changedSettings > 0) + insights.Add(new("changed-settings", "info", "Settings changed from their defaults", + "What is particular about this instance.", changedSettings, [new Item("Settings", "/settings")])); + + return new { Since = since, Insights = insights }; + } +} diff --git a/SW.Bitween.Api/Resources/Metrics/Nodes.cs b/SW.Bitween.Api/Resources/Metrics/Nodes.cs new file mode 100644 index 00000000..57e60bec --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Nodes.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json.Linq; +using SW.Bitween.Domain.Cluster; +using SW.Bitween.Services.Cluster; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// +/// What each node has done in the last day - exchanges, adapter calls and how they failed - and +/// which resident adapter instances it is running now, with what they use. A node whose failures +/// stand out from the rest is the one to look at. +/// +[HandlerName("nodes")] +public class Nodes(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) : IQueryHandler +{ + public async Task Handle() + { + await requestContext.EnsurePermission(db, Model.Permissions.Settings.View, Model.Permissions.Monitoring.View); + return await cache.Cached("nodes", Compute); + } + + async Task Compute() + { + var now = DateTime.UtcNow; + var to = MetricsRecorder.HourOf(now).AddHours(1); + var from = to.AddHours(-24); + var exchanges = (await MetricsQueries.Exchanges(db, from, to).GroupBy(s => s.Node).SumBy()).ToDictionary(t => t.Key); + var adapters = (await MetricsQueries.Adapters(db, from, to).GroupBy(s => s.Node).SumBy()).ToDictionary(t => t.Key); + var nodes = await db.Set().AsNoTracking().OrderBy(n => n.Id).ToListAsync(); + + return new + { + Nodes = nodes.Select(n => + { + var e = exchanges.GetValueOrDefault(n.Id.Length <= 100 ? n.Id : n.Id[..100]); + var a = adapters.GetValueOrDefault(n.Id.Length <= 100 ? n.Id : n.Id[..100]); + return new + { + n.Id, + n.Host, + Alive = now - n.LastSeenOn < NodeHeartbeat.GoneAfter, + LastSeenOn = MetricsQueries.Utc(n.LastSeenOn), + Exchanges = e?.Total ?? 0, + Failed = e?.Failed ?? 0, + SuccessRate = e?.SuccessRate, + P95Ms = e?.P95Ms, + AdapterCalls = a?.Calls ?? 0, + AdapterFailures = a?.Failures ?? 0, + Timeouts = a?.Timeouts ?? 0, + LimitStops = a?.LimitStops ?? 0, + Crashes = a?.Crashes ?? 0, + Resident = Parse(n.ResidentHealth), + }; + }), + }; + } + + static JToken Parse(string json) + { + if (string.IsNullOrWhiteSpace(json)) return new JArray(); + try + { + return JToken.Parse(json); + } + catch + { + return new JArray(); + } + } +} diff --git a/SW.Bitween.Api/Resources/Metrics/Overview.cs b/SW.Bitween.Api/Resources/Metrics/Overview.cs new file mode 100644 index 00000000..c21609ea --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Overview.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// +/// The dashboard's numbers: traffic by hour against the same hours a week ago, success and +/// end-to-end time, volume by day, the worst and busiest of everything, subscriptions gone quiet, +/// and the broker's backlog over the day. All from the hourly rows; nothing counts exchanges. +/// +[HandlerName("overview")] +public class Overview(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) : IQueryHandler +{ + public async Task Handle() + { + await requestContext.EnsurePermission(db, Model.Permissions.Dashboard.View); + return await cache.Cached("overview", Compute); + } + + async Task Compute() + { + var now = DateTime.UtcNow; + var to = MetricsRecorder.HourOf(now).AddHours(1); + + var hourly = await MetricsQueries.Exchanges(db, to.AddHours(-48), to).GroupBy(s => s.Hour).SumBy(); + var weekAgo = await MetricsQueries.Exchanges(db, to.AddHours(-48).AddDays(-7), to.AddDays(-7)).GroupBy(s => s.Hour).SumBy(); + var last24 = await Total(to.AddHours(-24), to); + var previous24 = await Total(to.AddHours(-48), to.AddHours(-24)); + var last7 = await Total(to.AddDays(-7), to); + var daily = await MetricsQueries.Exchanges(db, to.AddDays(-30), to).GroupBy(s => s.Hour.Date).SumBy(); + + var subscriptionNames = await db.Set().AsNoTracking().ToDictionaryAsync(s => s.Id, s => s.Name); + var partnerNames = await db.Set().AsNoTracking().ToDictionaryAsync(p => p.Id, p => p.Name); + + var bySubscription = await MetricsQueries.Exchanges(db, to.AddHours(-24), to).Where(s => s.SubscriptionId > 0) + .GroupBy(s => s.SubscriptionId).SumBy(); + var byWeekSubscription = await MetricsQueries.Exchanges(db, to.AddDays(-7), to).Where(s => s.SubscriptionId > 0) + .GroupBy(s => s.SubscriptionId).SumBy(); + var byPartner = await MetricsQueries.Exchanges(db, to.AddHours(-24), to).Where(s => s.PartnerId > 0) + .GroupBy(s => s.PartnerId).SumBy(); + var byAdapter = await MetricsQueries.Adapters(db, to.AddHours(-24), to).GroupBy(s => s.AdapterId).SumBy(); + + var health = await SubscriptionHealth.ComputeAsync(db, 7, now); + var backlog = await db.Set().AsNoTracking().Where(s => s.At >= now.AddHours(-24)) + .OrderBy(s => s.At).ToListAsync(); + + return new + { + Since = await MetricsQueries.SinceAsync(db), + Hourly = hourly.OrderBy(h => h.Key).Select(h => new + { + Hour = MetricsQueries.Utc(h.Key), h.Total, h.Succeeded, h.BadResponse, h.Failed, + }), + WeekAgo = weekAgo.OrderBy(h => h.Key).Select(h => new { Hour = MetricsQueries.Utc(h.Key.AddDays(7)), h.Total }), + Last24h = Summary(last24), + Previous24h = Summary(previous24), + Last7d = Summary(last7), + Daily = daily.OrderBy(d => d.Key).Select(d => new + { + Day = MetricsQueries.Utc(d.Key), d.Total, d.Succeeded, d.BadResponse, d.Failed, Bytes = d.BytesIn + d.BytesOut, + }), + TopFailing = bySubscription.Where(s => s.Failed > 0).OrderByDescending(s => s.Failed).Take(5).Select(s => new + { + SubscriptionId = s.Key, Name = subscriptionNames.GetValueOrDefault(s.Key), s.Failed, s.Total, + }), + BusiestSubscriptions = byWeekSubscription.OrderByDescending(s => s.Total).Take(5).Select(s => new + { + SubscriptionId = s.Key, Name = subscriptionNames.GetValueOrDefault(s.Key), s.Total, s.Failed, s.BadResponse, + }), + BusiestPartners = byPartner.OrderByDescending(p => p.Total).Take(5).Select(p => new + { + PartnerId = p.Key, Name = partnerNames.GetValueOrDefault(p.Key), p.Total, p.Failed, + }), + SlowestAdapters = byAdapter.Where(a => a.Calls >= 5).OrderByDescending(a => a.P95Ms).Take(5).Select(a => new + { + AdapterId = a.Key, a.Calls, a.P95Ms, a.FailureRate, + }), + Silent = health.Where(h => h.Status == "silent").OrderByDescending(h => h.QuietHours).Select(h => new + { + h.SubscriptionId, h.Name, h.Type, h.LastExchangeHour, h.QuietHours, h.UsualGapHours, + }), + Failing = health.Count(h => h.Status == "failing"), + Degraded = health.Count(h => h.Status == "degraded"), + Backlog = backlog.Select(s => new + { + At = MetricsQueries.Utc(s.At), s.QueueDepth, s.RetryBacklog, s.DeadLetterBacklog, + }), + }; + } + + async Task Total(DateTime from, DateTime to) => + (await MetricsQueries.Exchanges(db, from, to).GroupBy(_ => 1).SumBy()).FirstOrDefault() ?? new ExchangeTotals(); + + static object Summary(ExchangeTotals t) => new + { + t.Total, t.Succeeded, t.BadResponse, t.Failed, t.Retries, t.ManualRetries, t.SuccessRate, t.P50Ms, t.P95Ms, + Bytes = t.BytesIn + t.BytesOut, + }; +} diff --git a/SW.Bitween.Api/Resources/Metrics/Partners.cs b/SW.Bitween.Api/Resources/Metrics/Partners.cs new file mode 100644 index 00000000..b1337a76 --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Partners.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// Every partner's traffic over the window, when it was last seen, and when each of its keys was last used. +[HandlerName("partners")] +public class Partners(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) + : IQueryHandler +{ + public async Task Handle(MetricsWindowRequest request) + { + await requestContext.EnsurePermission(db, Model.Permissions.Partners.View); + var days = MetricsQueries.Days(request?.Days); + return await cache.Cached($"partners.{days}", () => Compute(days)); + } + + async Task Compute(int days) + { + var to = MetricsRecorder.HourOf(DateTime.UtcNow).AddHours(1); + var from = to.AddDays(-days); + var totals = (await MetricsQueries.Exchanges(db, from, to).Where(s => s.PartnerId > 0).GroupBy(s => s.PartnerId).SumBy()) + .ToDictionary(t => t.Key); + var last = await MetricsQueries.Exchanges(db, to.AddDays(-30), to).Where(s => s.PartnerId > 0 && s.Total > 0) + .GroupBy(s => s.PartnerId).Select(g => new { g.Key, Last = g.Max(x => x.Hour) }) + .ToDictionaryAsync(x => x.Key, x => MetricsQueries.Utc(x.Last)); + var keys = await db.Set().AsNoTracking() + .GroupBy(k => k.PartnerId).Select(g => new { g.Key, Last = g.Max(k => k.LastUsedOn) }) + .ToDictionaryAsync(x => x.Key, x => MetricsQueries.Utc(x.Last)); + var partners = await db.Set().AsNoTracking().Where(p => p.Id != Partner.SystemId) + .Select(p => new { p.Id, p.Name }).ToListAsync(); + + return new + { + Since = await MetricsQueries.SinceAsync(db), + Days = days, + Partners = partners.Select(p => + { + var t = totals.GetValueOrDefault(p.Id); + return new + { + PartnerId = p.Id, p.Name, + Exchanges = t?.Total ?? 0, Failed = t?.Failed ?? 0, BadResponse = t?.BadResponse ?? 0, + SuccessRate = t?.SuccessRate, Bytes = (t?.BytesIn ?? 0) + (t?.BytesOut ?? 0), + LastExchangeHour = last.TryGetValue(p.Id, out var l) ? l : (DateTime?)null, + KeyLastUsedOn = keys.TryGetValue(p.Id, out var k) ? k : (DateTime?)null, + }; + }).OrderByDescending(p => p.Exchanges), + }; + } +} diff --git a/SW.Bitween.Api/Resources/Metrics/SubscriptionDetail.cs b/SW.Bitween.Api/Resources/Metrics/SubscriptionDetail.cs new file mode 100644 index 00000000..498d80ac --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/SubscriptionDetail.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// +/// One subscription's numbers for its page: its health, traffic by hour over two days and by day +/// over the window, where failures came from, retries, nodes, and its most common errors. +/// +[HandlerName("subscription")] +public class SubscriptionDetail(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) + : IQueryHandler +{ + public async Task Handle(SubscriptionMetricsRequest request) + { + await requestContext.EnsurePermission(db, Model.Permissions.Subscriptions.View); + var days = MetricsQueries.Days(request?.Days); + var id = request?.Id ?? 0; + return await cache.Cached($"subscription.{id}.{days}", () => Compute(id, days)); + } + + async Task Compute(int id, int days) + { + var now = DateTime.UtcNow; + var to = MetricsRecorder.HourOf(now).AddHours(1); + var from = to.AddDays(-days); + var rows = MetricsQueries.Exchanges(db, from, to).Where(s => s.SubscriptionId == id); + + var total = (await rows.GroupBy(_ => 1).SumBy()).FirstOrDefault() ?? new ExchangeTotals(); + var hourly = await MetricsQueries.Exchanges(db, to.AddHours(-48), to).Where(s => s.SubscriptionId == id) + .GroupBy(s => s.Hour).SumBy(); + var daily = await rows.GroupBy(s => s.Hour.Date).SumBy(); + var nodes = await rows.GroupBy(s => s.Node).SumBy(); + var health = (await SubscriptionHealth.ComputeAsync(db, days, now)).FirstOrDefault(h => h.SubscriptionId == id); + + var key = id.ToString(); + var errorRows = db.Set().AsNoTracking() + .Where(e => e.Scope == ErrorStats.SubscriptionScope && e.ScopeKey == key && e.Hour >= from && e.Hour < to); + var errors = await errorRows.GroupBy(e => e.Fingerprint) + .Select(g => new { Fingerprint = g.Key, Count = g.Sum(x => (long)x.Count), LastSeenOn = g.Max(x => x.LastSeenOn) }) + .OrderByDescending(e => e.Count).Take(10).ToListAsync(); + var fingerprints = errors.Select(e => e.Fingerprint).ToList(); + var samples = (await errorRows.Where(e => fingerprints.Contains(e.Fingerprint)) + .Select(e => new { e.Fingerprint, e.Sample, e.LastSeenOn }).ToListAsync()) + .GroupBy(e => e.Fingerprint).ToDictionary(g => g.Key, g => g.OrderByDescending(e => e.LastSeenOn).First().Sample); + + return new + { + Id = id, + Days = days, + Since = await MetricsQueries.SinceAsync(db), + Status = health?.Status, + health?.LastExchangeHour, + health?.QuietHours, + health?.UsualGapHours, + Total = new + { + total.Total, total.Succeeded, total.BadResponse, total.Failed, total.FailedAtMapper, total.FailedAtHandler, + total.Retries, total.ManualRetries, total.SuccessRate, total.P50Ms, total.P95Ms, Bytes = total.BytesIn + total.BytesOut, + }, + Hourly = hourly.OrderBy(h => h.Key).Select(h => new { Hour = MetricsQueries.Utc(h.Key), h.Total, h.Failed, h.BadResponse }), + Daily = daily.OrderBy(d => d.Key).Select(d => new { Day = MetricsQueries.Utc(d.Key), d.Total, d.Failed, d.P95Ms }), + Nodes = nodes.OrderByDescending(n => n.Total).Select(n => new { Node = n.Key, n.Total, n.Failed }), + Errors = errors.Select(e => new + { + e.Fingerprint, e.Count, LastSeenOn = MetricsQueries.Utc(e.LastSeenOn), Sample = samples.GetValueOrDefault(e.Fingerprint), + }), + }; + } +} diff --git a/SW.Bitween.Api/Resources/Metrics/Subscriptions.cs b/SW.Bitween.Api/Resources/Metrics/Subscriptions.cs new file mode 100644 index 00000000..2ab1c8bc --- /dev/null +++ b/SW.Bitween.Api/Resources/Metrics/Subscriptions.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Metrics; + +/// Every subscription's health over the window: status, success, timing, retries, traffic by day. +[HandlerName("subscriptions")] +public class Subscriptions(BitweenDbContext db, RequestContext requestContext, IMemoryCache cache) + : IQueryHandler +{ + public async Task Handle(MetricsWindowRequest request) + { + await requestContext.EnsurePermission(db, Model.Permissions.Subscriptions.View); + var days = MetricsQueries.Days(request?.Days); + return await cache.Cached($"subscriptions.{days}", async () => + { + var rows = await SubscriptionHealth.ComputeAsync(db, days, DateTime.UtcNow); + return (object)new + { + Since = await MetricsQueries.SinceAsync(db), + Days = days, + Subscriptions = rows.Select(r => new + { + r.SubscriptionId, r.Name, r.Type, r.Status, r.SuccessRate, r.P95Ms, r.LastExchangeHour, + r.QuietHours, r.UsualGapHours, r.Daily, + r.Totals.Total, r.Totals.Failed, r.Totals.BadResponse, r.Totals.Retries, r.Totals.ManualRetries, + Bytes = r.Totals.BytesIn + r.Totals.BytesOut, + }), + }; + }); + } +} diff --git a/SW.Bitween.Api/Resources/Notifiers/Delete.cs b/SW.Bitween.Api/Resources/Notifiers/Delete.cs index d92cbcb8..e258a227 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Delete.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Delete.cs @@ -10,7 +10,7 @@ public class Delete(BitweenDbContext dbContext, RequestContext requestContext, I { /// /// No reference check, unlike an integration's delete: nothing has a foreign key to a - /// notifier. RunOnSubscriptions points the other way — the notifier names the + /// notifier. RunOnSubscriptions points the other way - the notifier names the /// integrations it watches, so deleting it takes the whole list with it. /// public async Task Handle(int key) diff --git a/SW.Bitween.Api/Resources/Notifiers/Get.cs b/SW.Bitween.Api/Resources/Notifiers/Get.cs index 211ee898..fd9eb8cf 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Get.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Get.cs @@ -8,7 +8,8 @@ namespace SW.Bitween.Resources.Notifiers { - public class Get(BitweenDbContext dbContext, RequestContext requestContext) : IGetHandler + public class Get(BitweenDbContext dbContext, RequestContext requestContext, AdapterSecretProperties secrets) + : IGetHandler { private readonly BitweenDbContext dbContext = dbContext; private readonly RequestContext requestContext = requestContext; @@ -27,15 +28,20 @@ public async Task Handle(int key) .Where(s => notifier.RunOnSubscriptions.Any(sub => sub == s.Id)) .ToListAsync(); } - - + + // The handler's [Secure] values - an SMTP password, an API key - go out as the + // sentinel, as they do for a subscription's handler. Update puts them back. + var handlerProperties = notifier.HandlerProperties is null + ? null + : await secrets.Mask(notifier.HandlerId, notifier.HandlerProperties); + return new { Id = notifier.Id, Name = notifier.Name, Inactive = notifier.Inactive, HandlerId = notifier.HandlerId, - HandlerProperties= notifier.HandlerProperties?.ToKeyAndValueCollection(), + HandlerProperties= handlerProperties?.ToKeyAndValueCollection(), RunOnSuccessfulResult= notifier.RunOnSuccessfulResult, RunOnBadResult = notifier.RunOnBadResult, RunOnFailedResult = notifier.RunOnFailedResult, diff --git a/SW.Bitween.Api/Resources/Notifiers/Update.cs b/SW.Bitween.Api/Resources/Notifiers/Update.cs index ebb9f904..0759da19 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Update.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Update.cs @@ -16,16 +16,27 @@ public async Task Handle(int key, NotifierUpdate request) var notifier = await dbContext.FindAsync(key); + // A sentinel means "keep the stored secret", which only makes sense for the handler it + // was stored for. Switching handlers drops it, so the secret can't follow the notifier + // to a handler that might send it somewhere else. + var newHandlerId = request.HandlerId ?? notifier.HandlerId; + var incoming = (request.HandlerProperties ?? []).ToDictionary(); + var stored = AdapterSecretProperties.MayKeepStoredSecrets(notifier.HandlerId, newHandlerId, + notifier.HandlerProperties, incoming) + ? notifier.HandlerProperties + : null; + notifier.Update(request.Name, request.RunOnSuccessfulResult, request.RunOnBadResult, request.RunOnFailedResult, - request.HandlerId ?? notifier.HandlerId, + newHandlerId, request.Inactive, request.RunOnSubscriptions?.Select(r => r.Id)?.ToArray()); // An absent list means none, as it does for a document's promoted properties // and a retry policy's groups. Left implicit it threw ArgumentNullException. - notifier.SetDictionaries((request.HandlerProperties ?? []).ToDictionary()); + notifier.SetDictionaries( + AdapterSecretProperties.Merge(stored, incoming)); await dbContext.SaveChangesAsync(); await cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/Ops/Background.cs b/SW.Bitween.Api/Resources/Ops/Background.cs new file mode 100644 index 00000000..6c679bea --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/Background.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Quartz; +using Quartz.Impl.Matchers; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Ops; + +/// +/// Bitween's own background work: the system jobs (retries, retention, clean-ups) with when each +/// last ran and runs next, and the outbox of messages waiting for the broker. Neither was visible: +/// a job that stopped firing, or an outbox backing up, showed only as its consequences. +/// +[HandlerName("background")] +public class Background(BitweenDbContext dbContext, RequestContext requestContext, + IScheduleRepository scheduleRepo, ISchedulerFactory schedulerFactory) : IQueryHandler +{ + static readonly (Type Type, string Name, string Does)[] SystemJobs = + [ + (typeof(RetryJob), "Automatic retries", "Re-runs failed exchanges whose retry policy says they are due."), + (typeof(ExchangeRetentionJob), "Exchange retention", "Deletes or archives exchanges older than they are kept."), + (typeof(ReceiveAttemptCleanupJob), "Receive attempt clean-up", "Deletes old records of scheduled jobs' receive attempts."), + (typeof(Services.DataSources.InboundMessagePruneJob), "Broker deduplication clean-up", "Forgets deduplication keys older than each data source keeps them."), + (typeof(Services.Metrics.QueueSnapshotJob), "Queue snapshots", "Records the broker's queue totals every few minutes, for the backlog trend."), + (typeof(Services.Metrics.MetricsPruneJob), "Metrics clean-up", "Deletes hourly metrics and queue snapshots past their keeping."), + (typeof(QuietSubscriptionsJob), "Quiet subscriptions", "Tells the notifiers when a subscription that expects traffic gets none, and when it comes back."), + ]; + + public async Task Handle() + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View, Model.Permissions.Dashboard.View); + + var scheduler = await schedulerFactory.GetScheduler(); + var running = (await scheduler.GetCurrentlyExecutingJobs()).Select(j => j.JobDetail.JobType).ToHashSet(); + var definitions = scheduleRepo.GetJobDefinitions().ToDictionary(d => d.JobType); + + var jobs = new List(); + foreach (var (type, name, does) in SystemJobs) + { + DateTime? next = null, previous = null; + string cron = null; + var states = new List(); + if (definitions.TryGetValue(type, out var definition)) + foreach (var key in await scheduler.GetJobKeys(GroupMatcher.GroupEquals(definition.Group))) + foreach (var trigger in await scheduler.GetTriggersOfJob(key)) + { + states.Add((await scheduler.GetTriggerState(trigger.Key)).ToString()); + var n = trigger.GetNextFireTimeUtc()?.UtcDateTime; + var p = trigger.GetPreviousFireTimeUtc()?.UtcDateTime; + if (n != null && (next == null || n < next)) next = n; + if (p != null && (previous == null || p > previous)) previous = p; + cron ??= (trigger as ICronTrigger)?.CronExpressionString; + } + + jobs.Add(new + { + Name = name, + Does = does, + Cron = cron, + Scheduled = states.Count > 0, + State = states.Count == 0 ? "NotScheduled" : states.Contains("Error") ? "Error" : states.Contains("Paused") ? "Paused" : "Normal", + LastRanOn = previous, + NextRunOn = next, + RunningNow = running.Contains(type), + }); + } + + var outbox = dbContext.Set().AsNoTracking(); + var pending = outbox.Where(m => m.PublishedOn == null); + var since = DateTime.UtcNow.AddHours(-1); + return new + { + Jobs = jobs, + Outbox = new + { + Pending = await pending.CountAsync(), + OldestPendingOn = await pending.OrderBy(m => m.CreatedOn).Select(m => (DateTime?)m.CreatedOn).FirstOrDefaultAsync(), + Failing = await pending.CountAsync(m => m.Attempts > 0), + LastError = await pending.Where(m => m.LastError != null).OrderByDescending(m => m.CreatedOn) + .Select(m => m.LastError).FirstOrDefaultAsync(), + PublishedLastHour = await outbox.CountAsync(m => m.PublishedOn >= since), + }, + }; + } +} diff --git a/SW.Bitween.Api/Resources/Ops/BrokerQueues.cs b/SW.Bitween.Api/Resources/Ops/BrokerQueues.cs index fd39b2eb..e6075512 100644 --- a/SW.Bitween.Api/Resources/Ops/BrokerQueues.cs +++ b/SW.Bitween.Api/Resources/Ops/BrokerQueues.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.Resources.Ops; /// /// Bitween's queues as the broker has them, through the RabbitMQ management API: the ones -/// nothing reads any more, and deleting a lane — a main queue with its retry and bad queues. +/// nothing reads any more, and deleting a lane - a main queue with its retry and bad queues. /// public class BrokerQueues(IBusDashboardDataService dashboardDataService, BusOptions busOptions, diff --git a/SW.Bitween.Api/Resources/Ops/DeadLetterMessages.cs b/SW.Bitween.Api/Resources/Ops/DeadLetterMessages.cs new file mode 100644 index 00000000..33d24c6e --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/DeadLetterMessages.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Ops; + +public class DeadLetterMessagesQuery +{ + /// A dead-letter queue, as /ops/deadletters names it. + public string Queue { get; set; } + public int Count { get; set; } = 20; +} + +/// +/// What is sitting in one dead-letter queue. The bodies are the messages themselves - a partner's +/// payload, often - so seeing them takes the right to see exchanges too. +/// +[HandlerName("DeadLetterMessages")] +public class DeadLetterMessages(DeadLetterQueues deadLetters, BitweenDbContext dbContext, RequestContext requestContext) + : IQueryHandler +{ + public async Task Handle(DeadLetterMessagesQuery request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.View); + await requestContext.EnsurePermission(dbContext, Model.Permissions.Exchanges.View); + + var queue = await deadLetters.Resolve(request.Queue) + ?? throw new SWNotFoundException("Dead-letter queue"); + return await deadLetters.Browse(queue, request.Count); + } +} diff --git a/SW.Bitween.Api/Resources/Ops/DeadLetterQueues.cs b/SW.Bitween.Api/Resources/Ops/DeadLetterQueues.cs new file mode 100644 index 00000000..288cf98d --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/DeadLetterQueues.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using SW.Bus.RabbitMqExtensions; + +namespace SW.Bitween.Resources.Ops; + +/// One message in a dead-letter queue, as the browse shows it. +public record DeadLetterMessageView( + int Position, + string Body, + bool BodyCut, + string LastException, + string CorrelationId, + string RoutingKey, + IReadOnlyList ExceptionHistory); + +/// +/// The messages the bus gave up on - each consumer's .bad queue - read without taking them +/// off, and moved back onto the consumer's own queue to be tried again. +/// +/// +/// A queue name is only ever matched against the bus's own dead-letter list, never handed to the +/// broker as given, so neither call can be pointed at a queue Bitween doesn't own. +/// +public class DeadLetterQueues( + IBusDashboardDataService dashboard, + IErrorQueueReader errorQueueReader, + IConfiguration configuration, + ILogger logger) +{ + public const int MaxBrowse = 50; + public const int MaxRequeue = 1000; + private const int MaxBodyChars = 64 * 1024; + + /// The queue as the bus names it, or null when it isn't one of its dead-letter queues. + public async Task Resolve(string queueName) + { + if (string.IsNullOrWhiteSpace(queueName)) return null; + var known = await dashboard.GetDeadLetterSummaryAsync(); + return known.Select(d => d.DeadLetterQueueName) + .FirstOrDefault(q => string.Equals(q, queueName.Trim(), StringComparison.OrdinalIgnoreCase)); + } + + /// The first messages, oldest first, left where they are. + public async Task> Browse(string queue, int count) + { + var messages = await errorQueueReader.PeekByQueueName(queue, Math.Clamp(count, 1, MaxBrowse)); + return messages.Select((m, i) => + { + var body = m.RawBody ?? ""; + return new DeadLetterMessageView( + i + 1, + body.Length > MaxBodyChars ? body[..MaxBodyChars] : body, + body.Length > MaxBodyChars, + m.LastException?.ToString(), + m.CorrelationId, + m.RoutingKey, + (m.ExceptionHistory ?? []).Select(e => e?.ToString()).ToList()); + }).ToList(); + } + + /// + /// Moves up to messages, oldest first, back onto the queue they failed + /// on, with a fresh retry count. Each one leaves the dead-letter queue only once the broker has + /// confirmed it is on the main queue, so a failure part-way loses nothing. + /// + /// How many were moved. + public int Requeue(string queue, int count) + { + var main = Regex.Replace(queue, @"\.bad$", "", RegexOptions.IgnoreCase); + var connectionString = configuration.GetConnectionString("RabbitMQ") + ?? throw new InvalidOperationException("No RabbitMQ connection string is configured."); + var factory = new ConnectionFactory + { + Uri = new Uri(connectionString), + AutomaticRecoveryEnabled = false, + RequestedConnectionTimeout = TimeSpan.FromSeconds(15) + }; + + using var connection = factory.CreateConnection("bitween-dead-letter-requeue"); + using var channel = connection.CreateModel(); + // Published straight to the queue: through its exchange, every other consumer of the same + // message would get it again too. And a queue that isn't there would swallow it. + channel.QueueDeclarePassive(main); + channel.ConfirmSelect(); + var returned = false; + channel.BasicReturn += (_, _) => returned = true; + + var moved = 0; + while (moved < Math.Clamp(count, 1, MaxRequeue)) + { + var message = channel.BasicGet(queue, autoAck: false); + if (message is null) break; + + var properties = message.BasicProperties; + if (properties.Headers is { } headers) + properties.Headers = headers.Where(h => !IsRetryState(h.Key)) + .ToDictionary(h => h.Key, h => h.Value); + + channel.BasicPublish("", main, mandatory: true, properties, message.Body); + channel.WaitForConfirmsOrDie(TimeSpan.FromSeconds(10)); + if (returned) + { + channel.BasicNack(message.DeliveryTag, multiple: false, requeue: true); + break; + } + + channel.BasicAck(message.DeliveryTag, multiple: false); + moved++; + } + + logger.LogInformation("Moved {Count} dead-lettered messages from {Queue} back to {Main}", moved, queue, main); + return moved; + } + + /// What the bus counts retries and records failures with: left on, the message would fail straight back. + private static bool IsRetryState(string header) => + header.StartsWith("x-death", StringComparison.OrdinalIgnoreCase) + || header.StartsWith("x-first-death", StringComparison.OrdinalIgnoreCase) + || header.StartsWith("x-last-death", StringComparison.OrdinalIgnoreCase) + || Regex.IsMatch(header, @"^exception\d*$", RegexOptions.IgnoreCase); +} diff --git a/SW.Bitween.Api/Resources/Ops/DeleteUnattendedQueues.cs b/SW.Bitween.Api/Resources/Ops/DeleteUnattendedQueues.cs index bb8e1482..3596ae8c 100644 --- a/SW.Bitween.Api/Resources/Ops/DeleteUnattendedQueues.cs +++ b/SW.Bitween.Api/Resources/Ops/DeleteUnattendedQueues.cs @@ -19,7 +19,7 @@ public record UnattendedQueueSkip(string QueueName, string Reason); public record DeleteUnattendedQueuesResult(List Deleted, List Skipped); /// -/// Deletes unattended lanes — each one's main queue and whichever of retry/bad exist — along with +/// Deletes unattended lanes - each one's main queue and whichever of retry/bad exist - along with /// whatever they hold. Names are only ever matched against a fresh unattended list, never passed to /// the broker as given, so this can't be pointed at a queue something still uses. A lane that can't /// go is skipped with the reason rather than failing the rest. @@ -71,6 +71,9 @@ public async Task Handle(DeleteUnattendedQueuesModel request) } } + if (deleted.Count > 0) + await dbContext.RecordNowAsync(requestContext, OperatorAction.DeleteQueues, string.Join(", ", deleted), + $"{deleted.Count} deleted, {skipped.Count} skipped"); return new DeleteUnattendedQueuesResult(deleted, skipped); } } diff --git a/SW.Bitween.Api/Resources/Ops/LaneResolver.cs b/SW.Bitween.Api/Resources/Ops/LaneResolver.cs index 11bcab58..26d47d87 100644 --- a/SW.Bitween.Api/Resources/Ops/LaneResolver.cs +++ b/SW.Bitween.Api/Resources/Ops/LaneResolver.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.Ops; /// /// What a consumed queue is for. The bus names queues after the consumer class and the -/// bus message type — xchangeservice.48low-result — and only Bitween knows that +/// bus message type - xchangeservice.48low-result - and only Bitween knows that /// 48low is work group 48. Resolving it here keeps the answer next to /// , which is the formula's owner; the UI used /// to rebuild that formula in TypeScript and match on the string. @@ -79,7 +79,7 @@ public LaneIdentity Resolve(string consumerName, string messageName) string.Equals(d.BusMessageTypeName, messageName, System.StringComparison.OrdinalIgnoreCase)); // A bus message only gets a queue while its information type is bus-enabled, so a - // name that resolves to nothing means the queue outlived it — and queues are never + // name that resolves to nothing means the queue outlived it - and queues are never // deleted. Reported as-is rather than hidden. return new LaneIdentity(QueueLane.FrontDoor, document?.Name ?? messageName, null, document?.Id); } @@ -100,7 +100,7 @@ public LaneIdentity Resolve(string consumerName, string messageName) ? messageName[..^XchangeService.ResultQueueSuffix.Length] : messageName; - // WorkGroup.None — id 0, so it can never collide with a real group. This is the lane + // WorkGroup.None - id 0, so it can never collide with a real group. This is the lane // everything without a work group shares. if (string.Equals(groupName, WorkGroup.None.GetBusMessageName(), System.StringComparison.OrdinalIgnoreCase)) return new LaneIdentity(lane, "Ungrouped", null, null); diff --git a/SW.Bitween.Api/Resources/Ops/RequeueDeadLetters.cs b/SW.Bitween.Api/Resources/Ops/RequeueDeadLetters.cs new file mode 100644 index 00000000..90393261 --- /dev/null +++ b/SW.Bitween.Api/Resources/Ops/RequeueDeadLetters.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using RabbitMQ.Client.Exceptions; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Ops; + +public class RequeueDeadLettersModel +{ + /// A dead-letter queue, as /ops/deadletters names it. + public string Queue { get; set; } + + /// How many, oldest first. Defaults to all of them, up to . + public int? Count { get; set; } +} + +public record RequeueDeadLettersResult(int Requeued); + +/// Sends dead-lettered messages back to the consumer that gave up on them, to be tried again. +[HandlerName("RequeueDeadLetters")] +public class RequeueDeadLetters(DeadLetterQueues deadLetters, BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(RequeueDeadLettersModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Monitoring.Operate); + + var queue = await deadLetters.Resolve(request.Queue) + ?? throw new SWNotFoundException("Dead-letter queue"); + int requeued; + try + { + requeued = deadLetters.Requeue(queue, request.Count ?? DeadLetterQueues.MaxRequeue); + } + catch (OperationInterruptedException) + { + throw new SWValidationException("NO_MAIN_QUEUE", + "The queue these messages failed on is gone, so there is nowhere to send them back to."); + } + + await dbContext.RecordNowAsync(requestContext, OperatorAction.RequeueDeadLetters, request.Queue, + $"{requeued} message{(requeued == 1 ? "" : "s")} sent back"); + return new RequeueDeadLettersResult(requeued); + } +} diff --git a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs index dc6f7c84..02d1eb87 100644 --- a/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs +++ b/SW.Bitween.Api/Resources/Ops/UnattendedQueues.cs @@ -14,8 +14,8 @@ namespace SW.Bitween.Resources.Ops; /// How many queues this lane is (main plus whichever of retry/bad exist). /// /// Consumers the broker reports across the lane's queues, from any instance. Above zero means -/// something still reads it — a listener that outlived its work group, or another instance -/// running different code during a deploy — so it isn't safe to delete. +/// something still reads it - a listener that outlived its work group, or another instance +/// running different code during a deploy - so it isn't safe to delete. /// /// /// Set when the lane is the queue of an information type paused on the bus: the type keeps its @@ -36,7 +36,7 @@ public record UnattendedQueueView( /// Every other Ops endpoint derives its list from the consumer definitions of the running /// process, so it can only ever show queues this instance already knows about. A paused /// information type's queue is one of these; so is anything left behind before deletes and -/// renames took their queues with them, or recreated empty by a client reconnecting — and they +/// renames took their queues with them, or recreated empty by a client reconnecting - and they /// vanish from those endpoints while keeping whatever they still hold. This is the only view /// that asks the broker instead of asking ourselves. /// diff --git a/SW.Bitween.Api/Resources/Partners/Delete.cs b/SW.Bitween.Api/Resources/Partners/Delete.cs index 21e48b6f..34a91448 100644 --- a/SW.Bitween.Api/Resources/Partners/Delete.cs +++ b/SW.Bitween.Api/Resources/Partners/Delete.cs @@ -28,7 +28,7 @@ public async Task Handle(int key) /// Says what still points at the partner, before the database says it less politely. /// /// - /// All three references are RESTRICT, so the delete was already refused — but as a + /// All three references are RESTRICT, so the delete was already refused - but as a /// foreign key violation surfacing as a bare 500, which names a constraint instead of the /// integration holding it and reads like a broken screen rather than a decision. /// Exchanges are deliberately not checked: they carry a partner id with no foreign key @@ -74,7 +74,7 @@ private async Task EnsureNothingPointsAtIt(int key) /// /// How many holders the message names before it stops listing them. A partner carrying - /// forty integrations would otherwise read out all forty into a dialog — longer to + /// forty integrations would otherwise read out all forty into a dialog - longer to /// understand than the short version, and forty rows fetched to build it. /// private const int MaxNamed = 5; diff --git a/SW.Bitween.Api/Resources/Partners/Get.cs b/SW.Bitween.Api/Resources/Partners/Get.cs index 3fdf9acb..d9486334 100644 --- a/SW.Bitween.Api/Resources/Partners/Get.cs +++ b/SW.Bitween.Api/Resources/Partners/Get.cs @@ -8,6 +8,13 @@ namespace SW.Bitween.Resources.Partners { + /// A partner as its page reads it: the update model, and when each key was last used. + public class PartnerDetail : PartnerUpdate + { + /// By key name. A key missing here hasn't had a call through since this was recorded. + public System.Collections.Generic.Dictionary KeysLastUsedOn { get; set; } = []; + } + public class Get(BitweenDbContext dbContext, RequestContext requestContext) : IGetHandler { private readonly BitweenDbContext dbContext = dbContext; @@ -19,14 +26,16 @@ async public Task Handle(int key) var partner = await dbContext.Set().AsNoTracking(). Search("Id", key). - Select(partner => new PartnerUpdate + Select(partner => new PartnerDetail { Name = partner.Name, ApiCredentials = partner.ApiCredentials.Select(cred => new KeyAndValue { Key = cred.Name, - Value = $"{cred.Key.Remove(5)}...(hidden)" + // Keys are stored hashed, so the prefix kept beside the hash is all there is + // to show. A short key used to throw here, too: Remove(5) on fewer than five. + Value = (cred.KeyPrefix ?? "") + "...(hidden)" }).ToList(), Subscriptions = partner.Subscriptions.Select(sub => new SubscriptionSearch @@ -46,8 +55,13 @@ async public Task Handle(int key) // The names come back so the form can draw the locks; the values behind them do not. if (partner != null) + { partner.AdapterProperties = AdapterSecretProperties.Mask(partner.AdapterProperties, partner.SecretProperties); + partner.KeysLastUsedOn = await dbContext.Set().AsNoTracking() + .Where(u => u.PartnerId == key) + .ToDictionaryAsync(u => u.KeyName, u => u.LastUsedOn); + } return partner; } diff --git a/SW.Bitween.Api/Resources/Partners/Search.cs b/SW.Bitween.Api/Resources/Partners/Search.cs index 8a83e5ee..2d1266ed 100644 --- a/SW.Bitween.Api/Resources/Partners/Search.cs +++ b/SW.Bitween.Api/Resources/Partners/Search.cs @@ -29,7 +29,7 @@ async public Task Handle(SearchyRequest searchyRequest, bool lookup = fa Name = subscriber.Name, // Every place the partner is wired in, matching the three groups the // detail page lists. Counting only the direct subscription link left - // "Used by" reading "—" for partners plainly in use through a gateway. + // "Used by" reading "-" for partners plainly in use through a gateway. SubscriptionsCount = subscriber.Subscriptions.Count + dbContext.Set().Count(g => g.PartnerId == subscriber.Id) + dbContext.Set().Count(r => r.PartnerId == subscriber.Id), diff --git a/SW.Bitween.Api/Resources/Partners/Update.cs b/SW.Bitween.Api/Resources/Partners/Update.cs index 9b50e20e..99594af1 100644 --- a/SW.Bitween.Api/Resources/Partners/Update.cs +++ b/SW.Bitween.Api/Resources/Partners/Update.cs @@ -17,7 +17,8 @@ public async Task Handle(int key, PartnerUpdate model) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Partners.Edit); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); entity.SetApiCredentials(model.ApiCredentials.Select(kv => new ApiCredential(kv.Key, kv.Value))); // Read while they are still there: SetProperties copies the model's own @@ -30,7 +31,9 @@ public async Task Handle(int key, PartnerUpdate model) // would overwrite its password with a row of dots. After SetProperties, which would // otherwise put the mask straight back. entity.AdapterProperties = - AdapterSecretProperties.Merge(storedProperties, model.AdapterProperties); + AdapterSecretProperties.Merge(storedProperties, model.AdapterProperties, + AdapterSecretProperties.MayKeepStoredSecrets(null, null, storedProperties, + model.AdapterProperties ?? new Dictionary())); entity.SecretProperties = model.SecretProperties?.ToList() ?? []; entity.LoginIdentity = await PartnerLoginIdentity.Validate(dbContext, model.LoginIdentity, key); await dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Permissions/Get.cs b/SW.Bitween.Api/Resources/Permissions/Get.cs index da2652f4..fb8a2202 100644 --- a/SW.Bitween.Api/Resources/Permissions/Get.cs +++ b/SW.Bitween.Api/Resources/Permissions/Get.cs @@ -5,7 +5,7 @@ namespace SW.Bitween.Resources.Permissions; /// -/// The permission catalog. Static data — the management UI builds its role matrix from this, so +/// The permission catalog. Static data - the management UI builds its role matrix from this, so /// the grants it offers can't drift from what the handlers actually enforce. /// public class Get : IQueryHandler diff --git a/SW.Bitween.Api/Resources/Retention/Aggregation.cs b/SW.Bitween.Api/Resources/Retention/Aggregation.cs index 1b207945..b48fcb6a 100644 --- a/SW.Bitween.Api/Resources/Retention/Aggregation.cs +++ b/SW.Bitween.Api/Resources/Retention/Aggregation.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Resources.Retention; /// -/// Whether retention would remove exchanges before an aggregation with these schedules collects them — +/// Whether retention would remove exchanges before an aggregation with these schedules collects them - /// shown on the aggregation as its schedule is edited, before it's saved. Changes nothing. /// [HandlerName("aggregation")] diff --git a/SW.Bitween.Api/Resources/Retention/Preview.cs b/SW.Bitween.Api/Resources/Retention/Preview.cs index 9da8e9b4..e5a4eba3 100644 --- a/SW.Bitween.Api/Resources/Retention/Preview.cs +++ b/SW.Bitween.Api/Resources/Retention/Preview.cs @@ -5,7 +5,7 @@ namespace SW.Bitween.Resources.Retention; /// -/// What retention settings someone is about to save would do, in the same shape as — +/// What retention settings someone is about to save would do, in the same shape as - /// shown before the change is confirmed. Changes nothing. /// [HandlerName("preview")] diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs index ca9e76fc..aa7f0544 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.RetryPolicies; /// -/// Lists the failures one group caught for one subscription — what a row of +/// Lists the failures one group caught for one subscription - what a row of /// spent its budget on. /// /// @@ -40,7 +40,7 @@ public async Task Handle(int key, RetryGroupAttemptsRequest request) // Both halves of the pair have to belong to the policy in the route. For the subscription // that keeps this from becoming a way to read any subscription's failures through any - // policy id; for the group it is about the answer being readable — an unknown group would + // policy id; for the group it is about the answer being readable - an unknown group would // otherwise report zero failures, which is indistinguishable from a group that genuinely // has none. var policy = await dbContext.Set().AsNoTracking() diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs index 036a4d80..626d1235 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs @@ -15,8 +15,8 @@ public async Task Handle(RetryPolicyCreate model) RetryGroupValidation.EnsureAlertTransportIsSecure( model.AlertHandlerId, model.AlertHandlerProperties); - // A new policy has nothing stored behind a sentinel, so any that arrives — from a policy - // copied out of Get, say — is dropped rather than saved as the literal password. + // A new policy has nothing stored behind a sentinel, so any that arrives - from a policy + // copied out of Get, say - is dropped rather than saved as the literal password. foreach (var group in model.Groups ?? []) AdapterSecretProperties.MergeInPlace(null, group.AlertHandlerProperties); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs index 7b711359..71036f33 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs @@ -23,7 +23,7 @@ public async Task Handle(int key) var policy = await dbContext.FindAsync(key); var groupIds = policy.Groups.Select(g => g.Id).ToList(); - // One change, one commit — same reasoning as Update: a half-done delete leaves rows keyed + // One change, one commit - same reasoning as Update: a half-done delete leaves rows keyed // by groups that no longer exist anywhere, which nothing can then reach. await using var transaction = await dbContext.Database.BeginTransactionAsync(); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs index 2586b33b..58008414 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.Resources.RetryPolicies; /// /// Rejects retry groups that could never fire. The evaluator skips such groups silently -/// (matchers must support the result type being evaluated — see +/// (matchers must support the result type being evaluated - see /// ), which reads as "retries just don't work", so the /// misconfiguration is caught at write time instead. /// @@ -35,7 +35,7 @@ public static void EnsureCanFire(IEnumerable groups) $"Group '{group.Name}' applies to {resultType} but none of its matchers can be " + $"evaluated against {resultType} content. {SupportedMatchersFor(resultType)}"); - // Allow with no budget has nothing to work from — no per-message cap, no total, no delay — + // Allow with no budget has nothing to work from - no per-message cap, no total, no delay - // so the evaluator can only refuse it. Caught here because a group saved that way silently // stops retrying, which reads as the whole feature being broken. if (group.Action == RetryAction.Allow && group.Budget == null) @@ -55,7 +55,7 @@ public static void EnsureCanFire(IEnumerable groups) } /// - /// Rejects an alert override that claims to send but names nothing to send with — the same trap + /// Rejects an alert override that claims to send but names nothing to send with - the same trap /// as guards at group level. /// public static void EnsureAlertCanSend(RetryAlertMode mode, string handlerId) @@ -71,14 +71,14 @@ public static void EnsureAlertCanSend(RetryAlertMode mode, string handlerId) /// /// /// - /// The handler refuses this at send time too, which is the guarantee that matters — properties + /// The handler refuses this at send time too, which is the guarantee that matters - properties /// can also arrive straight through the API or out of a global values set. Catching it here is /// so the person configuring it finds out when they save, rather than from a missing alert and a /// line in the log days later. /// /// /// Only the mail handler is named, because only it has a password. A general answer belongs in - /// the adapter contract — an adapter saying which of its own settings conflict — not here. + /// the adapter contract - an adapter saying which of its own settings conflict - not here. /// /// public static void EnsureAlertTransportIsSecure( @@ -88,7 +88,7 @@ public static void EnsureAlertTransportIsSecure( if (!nameof(NativeSmtpHandler).Equals(handlerId, StringComparison.OrdinalIgnoreCase)) return; // A masked password counts as set: the sentinel means one is stored, not that the field is - // empty. Only an explicit "false" turns encryption off — absent means the adapter's own + // empty. Only an explicit "false" turns encryption off - absent means the adapter's own // default, which is on. var password = Value(properties, nameof(SmtpHandlerInput.Password)); var useTls = Value(properties, nameof(SmtpHandlerInput.UseTls)); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs index ec15376f..236a2463 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Resources.RetryPolicies; /// -/// Sets, changes or clears where one subscription's alerts go for one group of this policy — the most +/// Sets, changes or clears where one subscription's alerts go for one group of this policy - the most /// specific level of the hierarchy. /// [HandlerName("savealertoverride")] @@ -44,7 +44,7 @@ public async Task Handle(int key, RetryAlertOverrideSave request) && o.GroupId == request.GroupId); // Inherit is the absence of an override, so store nothing rather than a row that does nothing - // — otherwise the routing list would have to explain a row that changes no behaviour. + // - otherwise the routing list would have to explain a row that changes no behaviour. if (request.AlertMode == RetryAlertMode.Inherit) { if (existing != null) dbContext.Remove(existing); @@ -54,8 +54,8 @@ public async Task Handle(int key, RetryAlertOverrideSave request) // A masked secret has to be restored from whichever level the caller was shown it at. Usage // masks two things for a pair: the override's own properties, and the properties of the level - // it currently inherits from. Overriding an inherited alert starts from the second — there is - // no override row yet — so both are offered here, with the override's own winning. + // it currently inherits from. Overriding an inherited alert starts from the second - there is + // no override row yet - so both are offered here, with the override's own winning. var group = policy.Groups.First(g => g.Id == request.GroupId); var inherited = RetryAlertResolver.Resolve(existing, group, policy); @@ -65,7 +65,10 @@ public async Task Handle(int key, RetryAlertOverrideSave request) foreach (var kv in existing?.AlertHandlerProperties ?? new Dictionary()) restoreFrom[kv.Key] = kv.Value; - var properties = AdapterSecretProperties.Merge(restoreFrom, request.AlertHandlerProperties); + var restoreFromHandler = existing?.AlertHandlerId ?? inherited?.HandlerId; + var properties = AdapterSecretProperties.Merge(restoreFrom, request.AlertHandlerProperties, + AdapterSecretProperties.MayKeepStoredSecrets(restoreFromHandler, request.AlertHandlerId, restoreFrom, + request.AlertHandlerProperties ?? new Dictionary())); if (existing == null) { diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs index 3aae6b5c..a9705ffd 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs @@ -21,7 +21,7 @@ public async Task Handle(TestRetryPolicyRequest request) if (request.ResultType == XchangeResultType.Success) throw new SWValidationException("INVALID_RESULT_TYPE", - "Choose Error or Bad result — a successful result is never retried."); + "Choose Error or Bad result - a successful result is never retried."); var policy = new CustomRetryPolicy { Groups = request.Groups ?? [] }; // In-memory budget: a dry-run must not spend any real integration's total. @@ -44,7 +44,7 @@ public async Task Handle(TestRetryPolicyRequest request) // Once blocked, every later attempt for this same message would be blocked // for the same reason (budgets never un-consume, and a Block action or "no - // match" is structural) — no value in simulating further. + // match" is structural) - no value in simulating further. if (!decision.ShouldRetry) break; } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs index 3b741918..a4c03041 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -17,10 +18,11 @@ public async Task Handle(int key, RetryPolicyUpdate model) RetryGroupValidation.EnsureAlertTransportIsSecure( model.AlertHandlerId, model.AlertHandlerProperties); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); // Spent budget is keyed by group id, so a group removed here would leave usage rows - // that no policy claims — invisible to the usage report and beyond the reach of reset. + // that no policy claims - invisible to the usage report and beyond the reach of reset. var removedGroupIds = entity.Groups .Select(g => g.Id) .Except((model.Groups ?? []).Select(g => g.Id)) @@ -29,7 +31,7 @@ public async Task Handle(int key, RetryPolicyUpdate model) // Dropping the groups and clearing what belonged to them is one change, so it commits as // one: a group no policy claims whose usage and override rows survive is unreachable from // the usage report and from reset alike. Safe to span, because RetryPolicy raises no domain - // events — nothing reaches the bus before the commit. + // events - nothing reaches the bus before the commit. await using var transaction = await dbContext.Database.BeginTransactionAsync(); // Secrets came out of Get masked, so put them back from what this same level already holds. @@ -37,17 +39,23 @@ public async Task Handle(int key, RetryPolicyUpdate model) foreach (var group in model.Groups ?? []) { var storedGroup = entity.Groups.FirstOrDefault(g => g.Id == group.Id); + var keep = storedGroup != null && AdapterSecretProperties.MayKeepStoredSecrets( + storedGroup.AlertHandlerId, group.AlertHandlerId, storedGroup.AlertHandlerProperties, + group.AlertHandlerProperties ?? new Dictionary()); AdapterSecretProperties.MergeInPlace( - storedGroup?.AlertHandlerProperties, group.AlertHandlerProperties); + keep ? storedGroup.AlertHandlerProperties : null, group.AlertHandlerProperties); } var storedPolicyProperties = entity.AlertHandlerProperties; + var storedPolicyHandlerId = entity.AlertHandlerId; entity.Name = model.Name; entity.Groups = model.Groups ?? []; entity.AlertHandlerId = model.AlertHandlerId; entity.AlertHandlerProperties = - AdapterSecretProperties.Merge(storedPolicyProperties, model.AlertHandlerProperties); + AdapterSecretProperties.Merge(storedPolicyProperties, model.AlertHandlerProperties, + AdapterSecretProperties.MayKeepStoredSecrets(storedPolicyHandlerId, model.AlertHandlerId, + storedPolicyProperties, model.AlertHandlerProperties ?? new Dictionary())); await dbContext.SaveChangesAsync(); if (removedGroupIds.Count > 0) diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs index ca69e623..2d3e7e61 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.Resources.RetryPolicies; /// /// /// -/// Both halves share the (SubscriptionId, GroupId) key, so they are reported together — the +/// Both halves share the (SubscriptionId, GroupId) key, so they are reported together - the /// question worth asking about an exhausted budget is whether anyone was told about it, and /// splitting that across two reports leaves the caller to join them by eye. /// diff --git a/SW.Bitween.Api/Resources/Roles/Create.cs b/SW.Bitween.Api/Resources/Roles/Create.cs index 3827c073..ee9e8b88 100644 --- a/SW.Bitween.Api/Resources/Roles/Create.cs +++ b/SW.Bitween.Api/Resources/Roles/Create.cs @@ -1,3 +1,4 @@ +using SW.Bitween.Resources.Accounts; using System.Threading.Tasks; using FluentValidation; using SW.Bitween.Domain.Accounts; @@ -13,6 +14,7 @@ public async Task Handle(RoleCreate model) await requestContext.EnsurePermission(dbContext, Model.Permissions.Roles.Create); RoleValidation.EnsureKnownPermissions(model.Permissions); + await GrantLimits.EnsureCallerHoldsPermissions(dbContext, requestContext, model.Permissions); await RoleValidation.EnsureNameIsFree(dbContext, model.Name); var role = new Role(model.Name, model.Description, model.Permissions); diff --git a/SW.Bitween.Api/Resources/Roles/Delete.cs b/SW.Bitween.Api/Resources/Roles/Delete.cs index fc4dcf36..9d8c277c 100644 --- a/SW.Bitween.Api/Resources/Roles/Delete.cs +++ b/SW.Bitween.Api/Resources/Roles/Delete.cs @@ -1,3 +1,4 @@ +using SW.Bitween.Resources.Accounts; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -19,6 +20,8 @@ public async Task Handle(int key) throw new SWValidationException("ROLE_IS_BUILT_IN", $"'{role.Name}' is a built-in role and can't be deleted."); + await GrantLimits.EnsureCallerHoldsPermissions(dbContext, requestContext, role.Permissions); + var memberCount = await dbContext.Set().CountAsync(l => l.RoleId == key); if (memberCount > 0) throw new SWValidationException("ROLE_IN_USE", diff --git a/SW.Bitween.Api/Resources/Roles/Update.cs b/SW.Bitween.Api/Resources/Roles/Update.cs index 0714e864..e703f0be 100644 --- a/SW.Bitween.Api/Resources/Roles/Update.cs +++ b/SW.Bitween.Api/Resources/Roles/Update.cs @@ -1,3 +1,5 @@ +using System.Linq; +using SW.Bitween.Resources.Accounts; using System.Threading.Tasks; using FluentValidation; using SW.Bitween.Model; @@ -14,13 +16,18 @@ public async Task Handle(int key, RoleUpdate model) var role = await RoleValidation.Load(dbContext, key); - // Built-in roles are the floor an instance can always fall back to — if Administrator + // Built-in roles are the floor an instance can always fall back to - if Administrator // could be edited, an admin could lock everyone out of members and roles for good. if (role.IsSystem) throw new SWValidationException("ROLE_IS_BUILT_IN", $"'{role.Name}' is a built-in role and can't be changed. Create a role instead."); RoleValidation.EnsureKnownPermissions(model.Permissions); + + // Both what the role grants now and what it would grant: editing a role you couldn't have + // created is as much a step past your own access as creating one. + await GrantLimits.EnsureCallerHoldsPermissions(dbContext, requestContext, + (role.Permissions ?? []).Concat(model.Permissions ?? [])); await RoleValidation.EnsureNameIsFree(dbContext, model.Name, key); role.Update(model.Name, model.Description, model.Permissions); diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/Create.cs b/SW.Bitween.Api/Resources/ServiceAccounts/Create.cs new file mode 100644 index 00000000..547655b2 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/Create.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// A service account, with the roles it signs in with. Its tokens are issued separately. +public class Create(BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(CreateServiceAccountModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Create); + + var name = request.Name.Trim(); + var roleIds = (request.RoleIds ?? []).Distinct().ToList(); + await ServiceAccountStore.EnsureNameFree(dbContext, name); + // Nobody makes an account that can do more than they can. + await GrantLimits.EnsureCallerHoldsRoles(dbContext, requestContext, roleIds); + + var account = Account.ForService(name, AccountRoles.LegacyRoleFor(roleIds)); + dbContext.Add(account); + await dbContext.SaveChangesAsync(); + + await AccountRoles.Set(dbContext, account.Id, roleIds); + await dbContext.SaveChangesAsync(); + return account.Id; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Name).NotEmpty().MaximumLength(200); + } + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/Delete.cs b/SW.Bitween.Api/Resources/ServiceAccounts/Delete.cs new file mode 100644 index 00000000..02359a14 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/Delete.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Resources.Accounts; +using SW.HttpExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// Removes a service account; its tokens are revoked with it. +public class Delete(BitweenDbContext dbContext, RequestContext requestContext) : IDeleteHandler +{ + public async Task Handle(int key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Delete); + + var account = await ServiceAccountStore.Load(dbContext, key); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + + var by = requestContext.GetNameIdentifier(); + foreach (var token in await dbContext.Set().Where(t => t.AccountId == key && t.RevokedOn == null).ToListAsync()) + token.Revoke(by); + + dbContext.Remove(account); + await dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/Get.cs b/SW.Bitween.Api/Resources/ServiceAccounts/Get.cs new file mode 100644 index 00000000..fb6f9ff5 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/Get.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +public class Get(BitweenDbContext dbContext, RequestContext requestContext) : IGetHandler +{ + public async Task Handle(int key) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.View); + var found = await ServiceAccountStore.Describe(dbContext, + dbContext.Set().Where(a => a.Kind == AccountKind.Service && a.Id == key)); + return found.FirstOrDefault() ?? throw new SWNotFoundException(key.ToString()); + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/IssueToken.cs b/SW.Bitween.Api/Resources/ServiceAccounts/IssueToken.cs new file mode 100644 index 00000000..c11ddb59 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/IssueToken.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// +/// A new token for a service account. The answer is the only place its value ever appears; after +/// this, Bitween holds only its hash. +/// +[HandlerName("issueToken")] +public class IssueToken(BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(int key, CreateServiceTokenModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Create); + + var account = await ServiceAccountStore.Load(dbContext, key); + // A token can do whatever its account can; issuing one is handing that over. + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + + var now = DateTime.UtcNow; + var active = await dbContext.Set() + .CountAsync(t => t.AccountId == key && t.RevokedOn == null && t.ExpiresOn > now); + if (active >= ServiceToken.MaxActivePerAccount) + throw new SWValidationException("TOO_MANY_TOKENS", + $"'{account.DisplayName}' already has {active} active tokens. Revoke one before issuing another."); + + var days = request.ExpiresInDays ?? ServiceToken.DefaultLifetimeDays; + var (token, value) = ServiceToken.Issue(key, request.Name.Trim(), now.AddDays(days)); + dbContext.Add(token); + await dbContext.SaveChangesAsync(); + + var issued = ServiceAccountStore.Describe(token, now); + issued.Token = value; + return issued; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Name).NotEmpty().MaximumLength(100); + RuleFor(i => i.ExpiresInDays).InclusiveBetween(1, ServiceToken.MaxLifetimeDays).When(i => i.ExpiresInDays is not null); + } + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/RevokeToken.cs b/SW.Bitween.Api/Resources/ServiceAccounts/RevokeToken.cs new file mode 100644 index 00000000..53b9835f --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/RevokeToken.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.HttpExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// +/// Ends one token. Sessions it was already traded for run out on their own within minutes; to end +/// those at once too, disable the account. +/// +[HandlerName("revokeToken")] +public class RevokeToken(BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(int key, RevokeServiceTokenModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Edit); + + await ServiceAccountStore.Load(dbContext, key); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + + var token = await dbContext.Set().SingleOrDefaultAsync(t => t.Id == request.TokenId && t.AccountId == key) + ?? throw new SWValidationException("TOKEN_NOT_FOUND", $"This service account has no token with the id {request.TokenId}"); + token.Revoke(requestContext.GetNameIdentifier()); + await dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/Search.cs b/SW.Bitween.Api/Resources/ServiceAccounts/Search.cs new file mode 100644 index 00000000..151045d8 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/Search.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +public class Search(BitweenDbContext dbContext, RequestContext requestContext) + : IQueryHandler +{ + public async Task Handle(SearchServiceAccountsModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.View); + return await ServiceAccountStore.Describe(dbContext, + dbContext.Set().Where(a => a.Kind == AccountKind.Service)); + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/ServiceAccountStore.cs b/SW.Bitween.Api/Resources/ServiceAccounts/ServiceAccountStore.cs new file mode 100644 index 00000000..dfd91442 --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/ServiceAccountStore.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +internal static class ServiceAccountStore +{ + public static async Task Load(BitweenDbContext dbContext, int id) + { + var account = await dbContext.Set().FindAsync(id); + if (account is not { IsService: true } || account.Deleted) + throw new SWValidationException("SERVICE_ACCOUNT_NOT_FOUND", $"No service account exists with the id {id}"); + return account; + } + + public static async Task EnsureNameFree(BitweenDbContext dbContext, string name, int? except = null) + { + if (await dbContext.Set().AnyAsync(a => + a.Kind == AccountKind.Service && a.DisplayName == name && (except == null || a.Id != except))) + throw new SWValidationException("SERVICE_ACCOUNT_EXISTS", $"There's already a service account named '{name}'."); + } + + public static string Status(ServiceToken token, DateTime nowUtc) => + token.RevokedOn is not null ? "revoked" : token.ExpiresOn <= nowUtc ? "expired" : "active"; + + public static T Describe(ServiceToken token, DateTime nowUtc) where T : ServiceTokenModel, new() => new() + { + Id = token.Id, + Name = token.Name, + Shown = token.Shown, + CreatedOn = token.CreatedOn, + CreatedBy = token.CreatedBy, + ExpiresOn = token.ExpiresOn, + RevokedOn = token.RevokedOn, + LastUsedOn = token.LastUsedOn, + LastUsedFrom = token.LastUsedFrom, + Status = Status(token, nowUtc), + }; + + public static async Task> Describe(BitweenDbContext dbContext, IQueryable accounts) + { + var now = DateTime.UtcNow; + var list = await accounts.AsNoTracking().OrderBy(a => a.DisplayName).ToListAsync(); + var ids = list.Select(a => a.Id).ToList(); + var roles = await AccountRoles.For(dbContext, ids); + var tokens = (await dbContext.Set().AsNoTracking() + .Where(t => ids.Contains(t.AccountId)).ToListAsync()) + .ToLookup(t => t.AccountId); + + return list.Select(a => new ServiceAccountModel + { + Id = a.Id, + Name = a.DisplayName, + Disabled = a.Disabled, + CreatedOn = a.CreatedOn, + LastSignInOn = a.LastSignInOn, + Roles = roles.GetValueOrDefault(a.Id, []), + // Active first, newest first; ended ones stay listed, for when they were used last. + Tokens = tokens[a.Id] + .OrderBy(t => Status(t, now) == "active" ? 0 : 1).ThenByDescending(t => t.CreatedOn) + .Select(t => Describe(t, now)).ToList(), + }).ToList(); + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/SetDisabled.cs b/SW.Bitween.Api/Resources/ServiceAccounts/SetDisabled.cs new file mode 100644 index 00000000..e813211b --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/SetDisabled.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// +/// Stops or restores every token the account holds at once. Its sessions end too: permissions are +/// read per call, and a disabled account holds none. +/// +[HandlerName("setDisabled")] +public class SetDisabled(BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(int key, SetServiceAccountDisabledModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Edit); + + var account = await ServiceAccountStore.Load(dbContext, key); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + + account.SetDisabled(request.Disabled); + await dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/ServiceAccounts/Update.cs b/SW.Bitween.Api/Resources/ServiceAccounts/Update.cs new file mode 100644 index 00000000..8008892e --- /dev/null +++ b/SW.Bitween.Api/Resources/ServiceAccounts/Update.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using SW.Bitween.Model; +using SW.Bitween.Resources.Accounts; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ServiceAccounts; + +/// Renames a service account and replaces its roles. +public class Update(BitweenDbContext dbContext, RequestContext requestContext) + : ICommandHandler +{ + public async Task Handle(int key, UpdateServiceAccountModel request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.ServiceAccounts.Edit); + + var account = await ServiceAccountStore.Load(dbContext, key); + var name = request.Name.Trim(); + var roleIds = (request.RoleIds ?? []).Distinct().ToList(); + await ServiceAccountStore.EnsureNameFree(dbContext, name, key); + await GrantLimits.EnsureCallerOutranks(dbContext, requestContext, key); + await GrantLimits.EnsureCallerHoldsRoles(dbContext, requestContext, roleIds); + + account.UpdateProfile(name); + account.SetRole(AccountRoles.LegacyRoleFor(roleIds)); + await AccountRoles.Set(dbContext, key, roleIds); + await dbContext.SaveChangesAsync(); + return null; + } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.Name).NotEmpty().MaximumLength(200); + } + } +} diff --git a/SW.Bitween.Api/Resources/Settings/About.cs b/SW.Bitween.Api/Resources/Settings/About.cs new file mode 100644 index 00000000..5f84b7ce --- /dev/null +++ b/SW.Bitween.Api/Resources/Settings/About.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using SW.PrimitiveTypes; +using SW.Serverless.Runtimes; + +namespace SW.Bitween.Resources.Settings; + +/// +/// What this Bitween is and how it is set up, as the node that answers sees it: its version, the +/// node, its database, storage and broker, its readiness checks, which adapter runtimes it has, and +/// the settings that shape behaviour but live in configuration rather than on the Settings page. +/// Secrets are never in it, only whether they are set. +/// +[HandlerName("about")] +public class About(BitweenDbContext dbContext, RequestContext requestContext, BitweenOptions options, + IConfiguration configuration, IServiceProvider services) : IQueryHandler +{ + static readonly DateTimeOffset StartedOn = DateTimeOffset.UtcNow; + + public async Task Handle() + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Settings.View); + + var limits = configuration.GetSection("Bitween:RateLimits"); + var runtimes = services.GetService(); + + return new + { + Version = BitweenInfo.Version(options).ToString(), + Build = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion, + Node = new + { + Name = Environment.MachineName, + StartedOn, + Runtime = RuntimeInformation.FrameworkDescription, + Os = RuntimeInformation.OSDescription, + DataSources = options.BusProvidersEnabled, + }, + Database = options.DatabaseType, + Storage = new { Provider = options.StorageProvider, options.AdapterPath, options.DocumentPrefix }, + Broker = new + { + options.QueuePrefix, + ManagementConfigured = !string.IsNullOrWhiteSpace(options.RabbitMqManagementUrl), + }, + Health = await HealthAsync(), + Adapters = new + { + Runtimes = runtimes == null + ? [] + : await Task.WhenAll(new[] { "dotnet", "python", "node" }.Select(async name => + { + var status = await runtimes.StatusAsync(name); + return new { Name = name, status.Available, status.Version, status.Reason }; + })), + Pip = await ToolAsync("pip", "python3", "-m", "pip", "--version"), + Npm = await ToolAsync("npm", "npm", "--version"), + CommandTimeoutSeconds = options.ServerlessCommandTimeout, + Editor = new + { + Dependencies = options.AdapterEditorDependencies, + MemoryMb = options.AdapterEditorMemoryMb, + CpuCores = options.AdapterEditorCpuCores, + }, + }, + Limits = new + { + SignInPerMinute = limits.GetValue("SignInPerMinute", 10), + RequestsPerMinute = limits.GetValue("RequestsPerMinute", 600), + FileLinksPerMinute = limits.GetValue("FileLinksPerMinute", 60000), + options.MaxResponseWaitSeconds, + options.MaxRetryChainDepth, + options.StaleRunAfterMinutes, + options.NotifierQuietMinutes, + }, + Network = new + { + options.PublicUrl, + options.BlockPrivateNetworkAddresses, + TrustedProxies = string.IsNullOrWhiteSpace(options.TrustedProxies) ? 0 : options.TrustedProxies.Split(',', ';').Length, + options.ExposeApiDocs, + CorsOrigins = options.CorsOrigins ?? [], + }, + Retention = new + { + options.ReceiveAttemptRetentionDays, + options.ReceiveAttemptCleanupCron, + options.InboundMessagePruneCron, + }, + Telemetry = new + { + OpenTelemetry = !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? configuration["OpenTelemetry:Endpoint"]), + }, + }; + } + + /// The readiness checks /health/ready runs: the database and the broker. + async Task HealthAsync() + { + var health = services.GetService(); + if (health == null) return Array.Empty(); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var report = await health.CheckHealthAsync(_ => true, timeout.Token); + return report.Entries.Select(e => new + { + Name = e.Key, + Status = e.Value.Status.ToString(), + DurationMs = (int)e.Value.Duration.TotalMilliseconds, + e.Value.Description, + Error = e.Value.Exception?.Message, + }).ToList(); + } + + static readonly Dictionary>> Tools = new(); + static readonly object ToolsGate = new(); + + /// Whether a tool the adapter editor needs answers, asked once per process. + static Task ToolAsync(string key, string file, params string[] arguments) + { + lock (ToolsGate) + { + if (!Tools.TryGetValue(key, out var lazy)) + Tools[key] = lazy = new Lazy>(() => ProbeAsync(file, arguments)); + return lazy.Value; + } + } + + static async Task ProbeAsync(string file, string[] arguments) + { + try + { + var start = new ProcessStartInfo(file) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; + foreach (var argument in arguments) start.ArgumentList.Add(argument); + using var process = Process.Start(start)!; + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var output = await process.StandardOutput.ReadToEndAsync(timeout.Token); + await process.WaitForExitAsync(timeout.Token); + return process.ExitCode == 0 + ? new { Available = true, Version = output.Trim().Split('\n')[0], Reason = (string)null } + : new { Available = false, Version = (string)null, Reason = $"exited with {process.ExitCode}" }; + } + catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or OperationCanceledException or InvalidOperationException) + { + return new { Available = false, Version = (string)null, Reason = $"'{file}' isn't installed on this node" }; + } + } +} diff --git a/SW.Bitween.Api/Resources/Settings/Config.cs b/SW.Bitween.Api/Resources/Settings/Config.cs index c6c5bff9..49e46b30 100644 --- a/SW.Bitween.Api/Resources/Settings/Config.cs +++ b/SW.Bitween.Api/Resources/Settings/Config.cs @@ -19,7 +19,7 @@ public class Config(BitweenOptions BitweenOptions, ThemeOptions themeOptions) : /// This endpoint answers before anyone signs in, because the sign-in page brands itself from /// it. A penetration test followed the default GithubLink from here to our public /// repository, and from there to the sample signing key and the default administrator password - /// — the whole chain started with a link nobody had configured. + /// - the whole chain started with a link nobody had configured. /// /// Withheld only at the default: a deployment that sets its own link is publishing its own /// address and gets it back untouched. So the footer keeps working wherever it was meant to, @@ -45,8 +45,8 @@ public async Task Handle() && !string.IsNullOrWhiteSpace(BitweenOptions.RabbitMqManagementUsername) && !string.IsNullOrWhiteSpace(BitweenOptions.RabbitMqManagementPassword), Theme = ThemeWithout(withheld), - // The product defaults, so the sign-in page — which has no session and can't read the - // settings list — can tell a brand value someone chose from one nobody has touched. + // The product defaults, so the sign-in page - which has no session and can't read the + // settings list - can tell a brand value someone chose from one nobody has touched. ThemeDefaults = SettingsService.DefaultsUnder("Theme.") .Where(kv => !withheld.Contains(Pascalize(kv.Key))) .ToDictionary(kv => kv.Key, kv => kv.Value) @@ -58,8 +58,8 @@ public async Task Handle() /// /// /// Built by reflection rather than written out property by property: a hand-written projection - /// silently drops whatever is added to next, and the symptom — one - /// brand value that will not apply — looks nothing like its cause. + /// silently drops whatever is added to next, and the symptom - one + /// brand value that will not apply - looks nothing like its cause. /// private Dictionary ThemeWithout(IReadOnlySet withheld) => typeof(ThemeOptions).GetProperties() diff --git a/SW.Bitween.Api/Resources/Settings/Delete.cs b/SW.Bitween.Api/Resources/Settings/Delete.cs index c78f5cde..f411a91c 100644 --- a/SW.Bitween.Api/Resources/Settings/Delete.cs +++ b/SW.Bitween.Api/Resources/Settings/Delete.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.Resources.Settings; /// -/// Resets one setting to the product default — the value the options class ships with. +/// Resets one setting to the product default - the value the options class ships with. /// /// The row is rewritten rather than deleted: a missing row is how startup recognises a key it has /// never imported, so dropping it would let configuration seep back in on the next boot. Reset @@ -47,11 +47,11 @@ public async Task Handle(string key) await dbContext.SaveChangesAsync(); - // Resetting an already-default setting is a no-op, not an error — the UI can fire this for + // Resetting an already-default setting is a no-op, not an error - the UI can fire this for // a staged reset it never saved a value for. settings.Apply(definition, productDefault); - // A reset has to take effect the same way a write does — see Update. + // A reset has to take effect the same way a write does - see Update. if (definition.OnChange is not null) await definition.OnChange(serviceProvider); await cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/Settings/Get.cs b/SW.Bitween.Api/Resources/Settings/Get.cs index 84614464..fa860a7a 100644 --- a/SW.Bitween.Api/Resources/Settings/Get.cs +++ b/SW.Bitween.Api/Resources/Settings/Get.cs @@ -11,8 +11,8 @@ namespace SW.Bitween.Resources.Settings; /// /// Every editable setting, in catalog order: the definition from -/// joined with the stored value. Rows normally exist for all of them — startup imports whatever -/// configuration had — so a missing row means the key couldn't be stored yet. +/// joined with the stored value. Rows normally exist for all of them - startup imports whatever +/// configuration had - so a missing row means the key couldn't be stored yet. /// public class Get(BitweenDbContext dbContext, RequestContext requestContext, SettingsService settings) : IQueryHandler @@ -35,7 +35,7 @@ public async Task Handle() // A secret's value is withheld either way round: only whether one is set is public. var value = definition.Secret ? null : hasRow ? stored : productDefault; // A secret has no product default, and its ciphertext couldn't be compared with one - // anyway — so for a secret both "is set" and "is overridden" mean the same thing: + // anyway - so for a secret both "is set" and "is overridden" mean the same thing: // a non-empty value is stored. var secretIsSet = hasRow && !string.IsNullOrEmpty(stored); diff --git a/SW.Bitween.Api/Resources/Settings/Update.cs b/SW.Bitween.Api/Resources/Settings/Update.cs index f47a2f6f..8063fd1c 100644 --- a/SW.Bitween.Api/Resources/Settings/Update.cs +++ b/SW.Bitween.Api/Resources/Settings/Update.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.Resources.Settings; /// Stores a new value for one setting. It's applied to the live options singletons as well as /// stored, so the very next request already sees it; the cache-revoke broadcast carries the change /// to any other instance. Secrets are encrypted before they're written. Only editable settings can -/// be written at all — the environment-owned ones the page also lists are rejected here. +/// be written at all - the environment-owned ones the page also lists are rejected here. /// public class Update( BitweenDbContext dbContext, @@ -36,7 +36,7 @@ public async Task Handle(string key, SettingUpdate request) $"{definition.Label} is a secret and can only be stored once " + $"{BitweenOptions.ConfigurationSection}:{nameof(BitweenOptions.SettingsEncryptionKey)} is configured."); - // Empty is a real value — it's how you clear an optional link or a license key. + // Empty is a real value - it's how you clear an optional link or a license key. var value = request?.Value ?? string.Empty; try @@ -51,7 +51,7 @@ public async Task Handle(string key, SettingUpdate request) await Store(definition, value); settings.Apply(definition, value); - // Some settings need more than the property assignment above — re-scheduling a job, for + // Some settings need more than the property assignment above - re-scheduling a job, for // instance. Runs after Apply so the hook reads the value that's now in effect. if (definition.OnChange is not null) await definition.OnChange(serviceProvider); diff --git a/SW.Bitween.Api/Resources/SubscriptionCategories/Create.cs b/SW.Bitween.Api/Resources/SubscriptionCategories/Create.cs index dc45345c..5ded1063 100644 --- a/SW.Bitween.Api/Resources/SubscriptionCategories/Create.cs +++ b/SW.Bitween.Api/Resources/SubscriptionCategories/Create.cs @@ -26,7 +26,7 @@ public async Task Handle(CreateSubscriptionCategoryModel request) } /// - /// The code is uniquely indexed, so a repeat was already refused — as a constraint violation + /// The code is uniquely indexed, so a repeat was already refused - as a constraint violation /// surfacing as a bare 500, which says nothing about the code being taken. /// internal static async Task EnsureCodeIsFree(BitweenDbContext dbContext, string code, int? existingId = null) diff --git a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs index 435d4f40..17f4e607 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs @@ -13,7 +13,8 @@ public async Task Handle(int key, SubscriptionAggregateNow request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.Operate); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); entity.SetAggregateNow(); await dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/ClearRunning.cs b/SW.Bitween.Api/Resources/Subscriptions/ClearRunning.cs new file mode 100644 index 00000000..31618bf8 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/ClearRunning.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Threading.Tasks; +using Quartz; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween.Resources.Subscriptions; + +public class SubscriptionClearRunning { } + +/// +/// Clears the running flag of a scheduled job or aggregation whose run died with its process. Until +/// it was cleared, every later run was skipped as "already running" for StaleRunAfterMinutes, and +/// the only other way out was editing the database. Refused while the job really is running here. +/// Saved through the context, so the audit trail says who cleared it. +/// +[HandlerName("clearrunning")] +public class ClearRunning(BitweenDbContext dbContext, RequestContext requestContext, + IScheduleRepository scheduleRepo, ISchedulerFactory schedulerFactory) + : ICommandHandler +{ + public async Task Handle(int key, SubscriptionClearRunning request) + { + await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.Operate); + + var entity = await dbContext.FindAsync(key) ?? throw new SWNotFoundException(key.ToString()); + if (!entity.IsRunning) return new { entity.Id, Cleared = false }; + + var scheduler = await schedulerFactory.GetScheduler(); + var jobs = scheduleRepo.GetJobDefinitions().ToDictionary(d => d.JobType); + if ((await GetScheduleHealth.CurrentlyRunningSubscriptionIds(scheduler, jobs)).Contains(key)) + throw new SWValidationException("STILL_RUNNING", + "This job is running right now on this node. Wait for it to finish; it clears the flag itself."); + + entity.ClearRunning(); + await dbContext.SaveChangesAsync(); + return new { entity.Id, Cleared = true }; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Create.cs b/SW.Bitween.Api/Resources/Subscriptions/Create.cs index 2eb3428c..2aab5fe6 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Create.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Create.cs @@ -82,7 +82,7 @@ private class Validate : AbstractValidator public Validate(BitweenDbContext dbContext, AdapterRequirements adapterRequirements) { // Same rule Documents enforces on BusMessageTypeName. Publishing to a name no - // information type carries is legitimate — the consumer may be another product — + // information type carries is legitimate - the consumer may be another product - // but it still becomes a RabbitMQ routing key, and a name with a space in it // could never be answered by an information type anyway. RuleFor(i => i.ResponseMessageTypeName) @@ -118,31 +118,39 @@ public Validate(BitweenDbContext dbContext, AdapterRequirements adapterRequireme When(i => i.Type == SubscriptionType.Aggregation, () => { RuleFor(i => i.AggregationForId).NotEmpty(); }); - // An adapter that is named has to be usable. Nothing is required to be named — - // create without a pipeline is still legitimate — but a half-configured adapter + // An adapter that is named has to be usable. Nothing is required to be named - + // create without a pipeline is still legitimate - but a half-configured adapter // is the failure this endpoint exists to stop committing. RuleFor(i => i.ReceiverProperties).CustomAsync(async (provided, context, _) => await AddMissing(context, adapterRequirements, - ((SubscriptionCreate)context.InstanceToValidate).ReceiverId, provided)); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref( + ((SubscriptionCreate)context.InstanceToValidate).ReceiverId, + ((SubscriptionCreate)context.InstanceToValidate).ReceiverVersion), provided)); RuleFor(i => i.ValidatorProperties).CustomAsync(async (provided, context, _) => await AddMissing(context, adapterRequirements, - ((SubscriptionCreate)context.InstanceToValidate).ValidatorId, provided)); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref( + ((SubscriptionCreate)context.InstanceToValidate).ValidatorId, + ((SubscriptionCreate)context.InstanceToValidate).ValidatorVersion), provided)); RuleFor(i => i.MapperProperties).CustomAsync(async (provided, context, _) => await AddMissing(context, adapterRequirements, - ((SubscriptionCreate)context.InstanceToValidate).MapperId, provided)); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref( + ((SubscriptionCreate)context.InstanceToValidate).MapperId, + ((SubscriptionCreate)context.InstanceToValidate).MapperVersion), provided)); RuleFor(i => i.HandlerProperties).CustomAsync(async (provided, context, _) => await AddMissing(context, adapterRequirements, - ((SubscriptionCreate)context.InstanceToValidate).HandlerId, provided)); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref( + ((SubscriptionCreate)context.InstanceToValidate).HandlerId, + ((SubscriptionCreate)context.InstanceToValidate).HandlerVersion), provided)); // Schedules only mean anything on the two scheduled types, and an empty set is // what Subscription.SetSchedules rejects outright. // // The type is the whole condition. Guarding on `Schedules != null` as well let the - // rule be skipped by the one case it most needed to catch — omitting the field - // entirely — so a scheduled job or aggregation saved with no trigger at all and + // rule be skipped by the one case it most needed to catch - omitting the field + // entirely - so a scheduled job or aggregation saved with no trigger at all and // then simply never ran. RuleFor(i => i.Schedules) .NotEmpty() diff --git a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs index 87cea8f4..4b335476 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs @@ -27,7 +27,7 @@ public async Task Handle(int key) /// Says what still points at the integration, before the database says it less politely. /// /// - /// All four references are RESTRICT, so the delete was already refused — but as a + /// All four references are RESTRICT, so the delete was already refused - but as a /// foreign key violation surfacing as a 500, which tells the operator nothing about which /// route or gateway is holding it, and reads like a broken screen rather than a decision. /// Exchanges are deliberately not checked: their reference is nullable and history is not diff --git a/SW.Bitween.Api/Resources/Subscriptions/Get.cs b/SW.Bitween.Api/Resources/Subscriptions/Get.cs index 3629eb66..a40ab02f 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Get.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Get.cs @@ -28,8 +28,11 @@ public async Task Handle(int key) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.View); + // A missing one is a 404, which the page says as "no longer exists"; it used to be a + // NullReferenceException and a 500. var subscriber = - await dbContext.Set().AsNoTracking().Search("Id", key).SingleOrDefaultAsync(); + await dbContext.Set().AsNoTracking().Search("Id", key).SingleOrDefaultAsync() + ?? throw new SWNotFoundException(key.ToString()); var response = new SubscriptionGet @@ -41,6 +44,10 @@ public async Task Handle(int key) Inactive = subscriber.Inactive, MapperId = subscriber.MapperId, ReceiverId = subscriber.ReceiverId, + HandlerVersion = subscriber.HandlerVersion, + MapperVersion = subscriber.MapperVersion, + ReceiverVersion = subscriber.ReceiverVersion, + ValidatorVersion = subscriber.ValidatorVersion, // Which data source every stage of this subscription runs through. Dropped // from this projection once, and the cost was quiet: the UI read every bound @@ -65,6 +72,10 @@ public async Task Handle(int key) AggregationTarget = subscriber.AggregationTarget, ValidatorId = subscriber.ValidatorId, PausedOn = subscriber.PausedOn, + PausedAutomatically = subscriber.PausedAutomatically, + AutoPauseAfterFailures = subscriber.AutoPauseAfterFailures, + ExpectTraffic = subscriber.ExpectTraffic, + QuietSince = subscriber.QuietSince, MatchExpression = subscriber.MatchExpression, CategoryDescription = subscriber.Category?.Description, CategoryCode = subscriber.Category?.Code, diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs b/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs index 5d86b6d1..5dadbd80 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/GetLastRuns.cs @@ -44,7 +44,7 @@ public async Task Handle(SearchSubscriptionLastRunsModel request) // One small indexed query per scheduled subscription. The subscription id // only exists inside the execution's JSON parameter, so there is nothing to - // GROUP BY — but each of these is an ordered top-N, and the alternative + // GROUP BY - but each of these is an ordered top-N, and the alternative // (pulling a whole retention window of every job's executions and grouping // in memory) is far worse on an instance with chatty schedules. // diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs index af5bda0e..e3dac1ba 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Resources.Subscriptions; /// /// Paged, filterable history of one Receiving subscription's own -/// rows — independent of , which reads the scheduler's own (always-succeeds) +/// rows - independent of , which reads the scheduler's own (always-succeeds) /// history instead. /// [HandlerName("receiveattempts")] diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs b/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs index b5ed29b5..c6686167 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/GetScheduleHealth.cs @@ -26,7 +26,7 @@ public class GetScheduleHealth( ISchedulerFactory schedulerFactory) : IQueryHandler { /// - /// Mirrors SW.Scheduler's internal Constants.JobParamsKey — the Quartz data-map + /// Mirrors SW.Scheduler's internal Constants.JobParamsKey - the Quartz data-map /// entry it serializes the job parameter under. Stable: it is persisted in /// qrtz_job_details, so changing it would break existing schedules. /// @@ -96,12 +96,12 @@ public async Task Handle(SearchSubscriptionScheduleHealthModel request) } /// - /// Subscription ids the scheduler is executing right now. This — not the - /// job-execution rows — is what distinguishes a genuinely running job from a + /// Subscription ids the scheduler is executing right now. This - not the + /// job-execution rows - is what distinguishes a genuinely running job from a /// stale IsRunning flag: a killed run never writes its end either, so its /// execution row stays open forever and would look identical. /// - private static async Task> CurrentlyRunningSubscriptionIds( + internal static async Task> CurrentlyRunningSubscriptionIds( IScheduler scheduler, Dictionary jobs) { var groups = jobs.Values.Select(d => d.Group).ToHashSet(); @@ -125,7 +125,7 @@ private static async Task> CurrentlyRunningSubscriptionIds( /// The state worth reporting: anything that stops a fire outranks the ones that don't. private static string WorstState(IReadOnlyCollection states, int scheduleCount) { - // No trigger where a schedule says there should be one — the job simply + // No trigger where a schedule says there should be one - the job simply // isn't registered with the scheduler and nothing will ever fire it. if (states.Count < scheduleCount) return "Missing"; if (states.Count == 0) return "Missing"; diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetSourcePaths.cs b/SW.Bitween.Api/Resources/Subscriptions/GetSourcePaths.cs index 8e9c4f0e..47872cfd 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/GetSourcePaths.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/GetSourcePaths.cs @@ -43,7 +43,7 @@ select x } catch (SWValidationException) { - // Its file is gone — deleted by retention, most likely. Nothing to offer, which is + // Its file is gone - deleted by retention, most likely. Nothing to offer, which is // what the editor shows for a subscription that hasn't run yet. } diff --git a/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs index 74e4dfb3..3679a26b 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Resources.Subscriptions { /// - /// Turns an integration defined inline — while a gateway route or attachment is being made — + /// Turns an integration defined inline - while a gateway route or attachment is being made - /// into a subscription, ready to be saved alongside whatever points at it. /// /// Nothing here duplicates : the pipeline is applied by the very same @@ -20,7 +20,7 @@ public static class InlineIntegration { /// /// Which integration a gateway link points at. Exactly one of an existing id and an - /// inline definition has to be given — both, or neither, is a mistake worth naming. + /// inline definition has to be given - both, or neither, is a mistake worth naming. /// public static void EnsureExactlyOne(int? subscriptionId, InlineIntegrationCreate inline) { @@ -56,7 +56,7 @@ private static async Task CheckConfiguration( throw new SWValidationException( ResponseRoutingValidation.Code, responseFailure); - // Neither gateway type carries its own partner — a partner reaches them through the + // Neither gateway type carries its own partner - a partner reaches them through the // attachment or the route, which is the very thing being made. if (model.PartnerId.HasValue) throw new SWValidationException("PARTNER_NOT_ALLOWED", @@ -66,10 +66,10 @@ private static async Task CheckConfiguration( // is exactly what committing here would make permanent. foreach (var (kind, adapterId, provided) in new[] { - ("receiver", model.ReceiverId, model.ReceiverProperties), - ("validator", model.ValidatorId, model.ValidatorProperties), - ("mapper", model.MapperId, model.MapperProperties), - ("handler", model.HandlerId, model.HandlerProperties), + ("receiver", SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(model.ReceiverId, model.ReceiverVersion), model.ReceiverProperties), + ("validator", SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(model.ValidatorId, model.ValidatorVersion), model.ValidatorProperties), + ("mapper", SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(model.MapperId, model.MapperVersion), model.MapperProperties), + ("handler", SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(model.HandlerId, model.HandlerVersion), model.HandlerProperties), }) { var missing = await adapterRequirements.MissingFor(adapterId, provided); @@ -110,8 +110,8 @@ public static async Task Stage( // and not the other. await SubscriptionConfigurationApplier.Apply(dbContext, adapterRequirements, entity, model); - // None of these types runs on its own — a GatewayApiCall waits for an attachment, a - // BusGateway for a route, a Response for a subscription to hand it a response — and the + // None of these types runs on its own - a GatewayApiCall waits for an attachment, a + // BusGateway for a route, a Response for a subscription to hand it a response - and the // one being made is in this same transaction. entity.Inactive = false; diff --git a/SW.Bitween.Api/Resources/Subscriptions/Pause.cs b/SW.Bitween.Api/Resources/Subscriptions/Pause.cs index 515ed28e..ebb91c80 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Pause.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Pause.cs @@ -15,7 +15,8 @@ public async Task Handle(int key, SubscriptionPause request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.Operate); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); if (entity!.PausedOn == null) entity.Pause(); else diff --git a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs index 5c7ff725..41e9ed27 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs @@ -13,7 +13,8 @@ async public Task Handle(int key, SubscriptionReceiveNow request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.Operate); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); entity.SetReceiveNow(); await dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs index 508b9685..7f31ea30 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs @@ -12,8 +12,8 @@ namespace SW.Bitween.Resources.Subscriptions; /// Guards what a delivery response may be fed into. /// /// Only a subscription, because it is the only type whose -/// entry point this is. Every other type has a door of its own — a gateway, a schedule, a -/// partner's call — and feeding it a response runs it through a door it does not have. A bus +/// entry point this is. Every other type has a door of its own - a gateway, a schedule, a +/// partner's call - and feeding it a response runs it through a door it does not have. A bus /// gateway route was the sharpest case: it ran with the bus skipped, so none of the other routes /// bound to the same message saw it. ResponseMessageTypeName is the field that actually /// puts a response on the bus. diff --git a/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs index 6505baa0..23a709ef 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.Resources.Subscriptions; /// /// /// The policy-scoped report answers the same question for every subscription sharing a policy, but it -/// can only find subscriptions by policy id — so a subscription carrying an inline +/// can only find subscriptions by policy id - so a subscription carrying an inline /// CustomRetryPolicy is invisible to it while still spending and recording budget. Asking from /// the subscription's side reaches those too. /// @@ -31,7 +31,7 @@ public async Task Handle(int key, RetryPolicyUsageRequest request) if (subscription == null) throw new SWNotFoundException(key.ToString()); // Whichever policy actually applies. An inline one has no row, so the policy level of the - // alert hierarchy simply is not there for it — passed as null, which the resolver expects. + // alert hierarchy simply is not there for it - passed as null, which the resolver expects. var groups = subscription.CustomRetryPolicy?.Groups ?? subscription.RetryPolicy?.Groups ?? []; return await report.Build( diff --git a/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs b/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs index d7cc4502..7088603a 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/SaveMapper.cs @@ -15,7 +15,8 @@ public class SaveMapper(BitweenDbContext dbContext, IInfolinkCache BitweenCache, public async Task Handle(int key, SubscriptionSaveMapper model) { await requestContext.EnsurePermission(dbContext, Model.Permissions.Subscriptions.Edit); - var entity = await dbContext.FindAsync(key); + var entity = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); entity.MapperId = model.MapperId; entity.SetDictionaries( diff --git a/SW.Bitween.Api/Resources/Subscriptions/Search.cs b/SW.Bitween.Api/Resources/Subscriptions/Search.cs index 902a85a9..639ed46d 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Search.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Search.cs @@ -16,12 +16,14 @@ public class Search : ISearchyHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly AdapterSecretProperties _secrets; private readonly List _edgeCaseProperties; - public Search(BitweenDbContext dbContext, RequestContext requestContext) + public Search(BitweenDbContext dbContext, RequestContext requestContext, AdapterSecretProperties secrets) { _dbContext = dbContext; _requestContext = requestContext; + _secrets = secrets; _edgeCaseProperties = new List { @@ -54,15 +56,23 @@ join document in _dbContext.Set() on subscriber.DocumentId equals docu MapperId = subscriber.MapperId, ValidatorId = subscriber.ValidatorId, ReceiverId = subscriber.ReceiverId, + HandlerVersion = subscriber.HandlerVersion, + MapperVersion = subscriber.MapperVersion, + ReceiverVersion = subscriber.ReceiverVersion, + ValidatorVersion = subscriber.ValidatorVersion, AggregationForId = subscriber.AggregationForId, Temporary = subscriber.Temporary, ReceiveOn = subscriber.ReceiveOn, // The next-fire time of the other scheduled type. Left out, an aggregation - // row had no next run to show at all — ReceiveOn is only ever set for + // row had no next run to show at all - ReceiveOn is only ever set for // Receiving, and the list page reads one field for both. AggregateOn = subscriber.AggregateOn, AggregationTarget = subscriber.AggregationTarget, PausedOn = subscriber.PausedOn, + PausedAutomatically = subscriber.PausedAutomatically, + AutoPauseAfterFailures = subscriber.AutoPauseAfterFailures, + ExpectTraffic = subscriber.ExpectTraffic, + QuietSince = subscriber.QuietSince, IsRunning = subscriber.IsRunning, ConsecutiveFailures = subscriber.ConsecutiveFailures, LastException = subscriber.LastException, @@ -117,7 +127,7 @@ join document in _dbContext.Set() on subscriber.DocumentId equals docu /// Empties the adapter property collections on every returned row. /// - /// Adapter properties hold credentials — a storage key, an OAuth secret, the password for a + /// Adapter properties hold credentials - a storage key, an OAuth secret, the password for a /// partner system. Subscriptions/Get replaces those with a sentinel before answering; /// this list endpoint returned them verbatim, which put every integration's secrets one /// request away from anyone allowed to see the list at all. @@ -125,12 +135,12 @@ join document in _dbContext.Set() on subscriber.DocumentId equals docu /// Emptied here rather than left out of the projection because the projection needs them: /// the rawsubscriptionproperties filter searches inside their values, in memory, over /// the rows this query produced. So they are loaded, used, and then dropped before the - /// response is shaped. Nothing reads them off this endpoint — both UIs configure a + /// response is shaped. Nothing reads them off this endpoint - both UIs configure a /// subscription from Subscriptions/Get, and the list has never shown them. /// /// /// Emptied rather than masked because masking can only hide what an adapter declares - /// [Secure], and the external adapters declare nothing — masking here would have + /// [Secure], and the external adapters declare nothing - masking here would have /// looked like a fix while leaving those integrations exactly as exposed. /// /// @@ -149,7 +159,7 @@ private static void StripAdapterProperties(List rows) /// /// A second query rather than part of the projection above: Schedule.On is a /// stored as ticks, and reading .Days/.Hours/ - /// .Minutes off it inside that joined query is what Postgres cannot translate — a + /// .Minutes off it inside that joined query is what Postgres cannot translate - a /// date_part type mismatch. Read flat for the ids actually being returned and shaped in /// memory, which asks nothing of the translator, so the list can finally say when a job /// runs instead of leaving its schedule column blank. @@ -194,14 +204,18 @@ private async Task> SearchWithEdgeCases( { var searchTerm = edgeCaseFilter.ValueString.ToLower(); + if (edgeCaseFilter.Field.ToLower() == "rawsubscriptionproperties") + { + var matched = new List(); + foreach (var row in data) + if (await AnyVisiblePropertyContains(row, searchTerm)) + matched.Add(row); + data = matched; + continue; + } + data = edgeCaseFilter.Field.ToLower() switch { - "rawsubscriptionproperties" => data.Where(i => - i.HandlerProperties.Any(p => p.Value.ToLower().Contains(searchTerm)) || - i.ReceiverProperties.Any(p => p.Value.ToLower().Contains(searchTerm)) || - i.MapperProperties.Any(p => p.Value.ToLower().Contains(searchTerm)) || - i.ValidatorProperties.Any(p => p.Value.ToLower().Contains(searchTerm))) - .ToList(), "rawfiltersproperties" => data.Where(i => i.DocumentFilter.Any(p => p.Value.ToLower().Contains(searchTerm)) || (i.MatchExpression?.ToString()?.Contains(searchTerm) ?? false)) @@ -212,7 +226,7 @@ private async Task> SearchWithEdgeCases( }; } - // Only the page being returned, same as the normal path — the edge-case filters run + // Only the page being returned, same as the normal path - the edge-case filters run // over the whole set in memory, and shaping schedules for all of it would be waste. var page = data.Skip(searchyRequest.PageSize * searchyRequest.PageIndex) .Take(searchyRequest.PageSize).ToList(); @@ -227,6 +241,39 @@ private async Task> SearchWithEdgeCases( } + /// + /// Matches only what Subscriptions/Get would show. Matching the stored values let a + /// viewer recover a masked secret one character at a time: search "a", "ab", "abc" and + /// watch which subscriptions stay in the result. + /// + private async Task AnyVisiblePropertyContains(SubscriptionSearch row, string searchTerm) + { + var bags = new (string AdapterId, ICollection Properties)[] + { + (row.HandlerId, row.HandlerProperties), + (row.ReceiverId, row.ReceiverProperties), + (row.MapperId, row.MapperProperties), + (row.ValidatorId, row.ValidatorProperties), + }; + + foreach (var (adapterId, properties) in bags) + { + if (properties is null || properties.Count == 0) continue; + + var stored = properties + .Where(p => p.Key is not null) + .GroupBy(p => p.Key) + .ToDictionary(g => g.Key, g => g.Last().Value); + var visible = await _secrets.Mask(adapterId, stored); + + if (visible.Values.Any(v => v is not null && v != AdapterSecretProperties.Sentinel && + v.ToLower().Contains(searchTerm))) + return true; + } + + return false; + } + private ICollection HandleSearchyEdgeCases(ICollection filters) { var list = new List(); diff --git a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs index f4bb1cbe..f3a683fe 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionConfigurationApplier.cs @@ -10,12 +10,12 @@ namespace SW.Bitween.Resources.Subscriptions; /// /// Puts a onto a subscription. Create and Update both -/// go through here, so a field cannot be honoured by one and silently ignored by the other — +/// go through here, so a field cannot be honoured by one and silently ignored by the other - /// which is what happened for as long as create couldn't accept a pipeline at all. /// /// Deliberately does not touch name, document, partner or aggregation-for. On create those are /// decided by the constructor, and an Aggregation subscription's DocumentId is -/// rather than anything the caller sent — copying +/// rather than anything the caller sent - copying /// the model over it would corrupt the subscription. /// /// @@ -30,6 +30,37 @@ internal static class SubscriptionConfigurationApplier public static async Task Apply(BitweenDbContext dbContext, AdapterRequirements adapterRequirements, Subscription entity, SubscriptionConfiguration model) { + // Read before they are overwritten: whether a stored secret may be kept depends on the + // adapter it was stored for. See AdapterSecretProperties.MayKeepStoredSecrets. + var (oldHandler, oldMapper, oldReceiver, oldValidator) = + (entity.HandlerId, entity.MapperId, entity.ReceiverId, entity.ValidatorId); + var (oldHandlerRef, oldMapperRef, oldReceiverRef, oldValidatorRef) = + (entity.HandlerRef, entity.MapperRef, entity.ReceiverRef, entity.ValidatorRef); + + // A pin without an adapter means nothing, and a blank one means "follow current". + static string Pin(string adapterId, string version) => + string.IsNullOrWhiteSpace(adapterId) || string.IsNullOrWhiteSpace(version) ? null : version.Trim(); + + var pins = ( + Handler: Pin(model.HandlerId, model.HandlerVersion), + Mapper: Pin(model.MapperId, model.MapperVersion), + Receiver: Pin(model.ReceiverId, model.ReceiverVersion), + Validator: Pin(model.ValidatorId, model.ValidatorVersion)); + + static bool Changed(string before, string adapterId, string version) => + !string.Equals(before, SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(adapterId, version), + System.StringComparison.OrdinalIgnoreCase); + + await adapterRequirements.EnsureRunnable("handler", model.HandlerId, pins.Handler, Changed(oldHandlerRef, model.HandlerId, pins.Handler)); + await adapterRequirements.EnsureRunnable("mapper", model.MapperId, pins.Mapper, Changed(oldMapperRef, model.MapperId, pins.Mapper)); + await adapterRequirements.EnsureRunnable("receiver", model.ReceiverId, pins.Receiver, Changed(oldReceiverRef, model.ReceiverId, pins.Receiver)); + await adapterRequirements.EnsureRunnable("validator", model.ValidatorId, pins.Validator, Changed(oldValidatorRef, model.ValidatorId, pins.Validator)); + + entity.HandlerVersion = pins.Handler; + entity.MapperVersion = pins.Mapper; + entity.ReceiverVersion = pins.Receiver; + entity.ValidatorVersion = pins.Validator; + entity.ReceiverId = model.ReceiverId; entity.ValidatorId = model.ValidatorId; entity.MapperId = model.MapperId; @@ -51,7 +82,13 @@ public static async Task Apply(BitweenDbContext dbContext, AdapterRequirements a } entity.ResponseMessageTypeName = model.ResponseMessageTypeName; entity.RunOnBadResponses = model.RunOnBadResponses; - // Meaningless for every other type, where it stays at its default — but harmless + if (model.AutoPauseAfterFailures is < 1 or > 1000) + throw new SWValidationException("AutoPauseAfterFailures", + "Pause after between 1 and 1000 failures in a row, or leave it empty to never pause."); + entity.AutoPauseAfterFailures = model.AutoPauseAfterFailures; + // Replaced like the fields around it: an update that leaves it out stops expecting traffic. + entity.SetExpectTraffic(TrafficWindow.Normalize(model.ExpectTraffic)); + // Meaningless for every other type, where it stays at its default - but harmless // there, and applying it unconditionally is what stops create and update disagreeing. entity.AggregationTarget = model.AggregationTarget; @@ -63,11 +100,11 @@ public static async Task Apply(BitweenDbContext dbContext, AdapterRequirements a System.TimeSpan.Parse($"{dto.Days}.{dto.Hours}:{dto.Minutes}:0"), dto.Backwards)).ToList()); entity.SetDictionaries( - MergeWithOriginal(entity.HandlerProperties, model.HandlerProperties), - MergeWithOriginal(entity.MapperProperties, model.MapperProperties), - MergeWithOriginal(entity.ReceiverProperties, model.ReceiverProperties), + MergeWithOriginal(entity.HandlerProperties, model.HandlerProperties, oldHandler, model.HandlerId), + MergeWithOriginal(entity.MapperProperties, model.MapperProperties, oldMapper, model.MapperId), + MergeWithOriginal(entity.ReceiverProperties, model.ReceiverProperties, oldReceiver, model.ReceiverId), model.DocumentFilter?.ToDictionary() ?? new Dictionary(), - MergeWithOriginal(entity.ValidatorProperties, model.ValidatorProperties) + MergeWithOriginal(entity.ValidatorProperties, model.ValidatorProperties, oldValidator, model.ValidatorId) ); entity.SetMatchExpression(model.MatchExpression); @@ -81,8 +118,12 @@ public static async Task Apply(BitweenDbContext dbContext, AdapterRequirements a private static Dictionary MergeWithOriginal( IReadOnlyDictionary original, - ICollection incoming) + ICollection incoming, string originalAdapterId, string adapterId) { + if (!AdapterSecretProperties.MayKeepStoredSecrets(originalAdapterId, adapterId, original, + (incoming ?? []).Select(kv => new KeyValuePair(kv.Key, kv.Value)))) + original = null; + var result = new Dictionary(); foreach (var kv in incoming ?? Enumerable.Empty()) { diff --git a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs index 4618cfb7..3d25b9ee 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/SubscriptionRunHistory.cs @@ -28,12 +28,12 @@ public static IQueryable Query( BitweenDbContext dbContext, IScheduledJobDefinition job, int subscriptionId, DateTime since) { // Executions are keyed per schedule entry, and a manual run gets its own - // generated key entirely — so neither the subscription id nor the set of + // generated key entirely - so neither the subscription id nor the set of // current schedule keys can be matched on JobName. The subscription id is // in the serialized job parameter instead, which covers all of them: // scheduled runs, manual runs, and runs of schedules since edited away. // - // The trailing comma keeps 128 from matching 1289 — safe because + // The trailing comma keeps 128 from matching 1289 - safe because // ReceivingJobParams/AggregationJobParams both carry CronExpression after // SubscriptionId, so it is never the last property. var needle = $"\"subscriptionId\":{subscriptionId},"; diff --git a/SW.Bitween.Api/Resources/Subscriptions/Update.cs b/SW.Bitween.Api/Resources/Subscriptions/Update.cs index e76723f8..292ae304 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Update.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Update.cs @@ -120,8 +120,10 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce { RuleFor(i => i.MapperProperties).CustomAsync(async (provided, context, _) => { + var update = (SubscriptionUpdate)context.InstanceToValidate; + // The pinned version's properties, which may differ from the current one's. var missing = await adapterRequirements.MissingFor( - ((SubscriptionUpdate)context.InstanceToValidate).MapperId, provided); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(update.MapperId, update.MapperVersion), provided); if (missing.Count > 0) context.AddFailure($"Missing: {string.Join(",", missing)}"); }); @@ -131,8 +133,9 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce { RuleFor(i => i.HandlerProperties).CustomAsync(async (provided, context, _) => { + var update = (SubscriptionUpdate)context.InstanceToValidate; var missing = await adapterRequirements.MissingFor( - ((SubscriptionUpdate)context.InstanceToValidate).HandlerId, provided); + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(update.HandlerId, update.HandlerVersion), provided); if (missing.Count > 0) context.AddFailure($"Missing: {string.Join(",", missing)}"); }); @@ -169,7 +172,8 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce if (model.Schedules == null || !model.Schedules.Any()) context.AddFailure(nameof(model.Schedules), "Schedules are required for Receiving subscriptions"); - var missing = await adapterRequirements.MissingFor(model.ReceiverId, model.ReceiverProperties); + var missing = await adapterRequirements.MissingFor( + SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(model.ReceiverId, model.ReceiverVersion), model.ReceiverProperties); if (missing.Count > 0) context.AddFailure(nameof(model.ReceiverProperties), $"Missing properties: {string.Join(",", missing)}"); } diff --git a/SW.Bitween.Api/Resources/WorkGroups/Create.cs b/SW.Bitween.Api/Resources/WorkGroups/Create.cs index 0b69a163..09839f13 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Create.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Create.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentValidation; +using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -12,6 +13,7 @@ public class Create(BitweenDbContext dbContext, RequestContext requestContext,II public async Task Handle(CreateWorkGroupModel request) { await requestContext.EnsurePermission(dbContext, Model.Permissions.WorkGroups.Create); + await EnsureNameIsFree(dbContext, request.Name); var workgroup = new WorkGroup() { @@ -36,6 +38,19 @@ public async Task Handle(CreateWorkGroupModel request) }; } + /// + /// Work groups are told apart by name everywhere but the database: the pickers, bitween apply. + /// Two applies running at once each made "Acme ERP" in the lab, split its subscriptions between + /// the two, and every apply after refused to go on until one was removed by hand. + /// + internal static async Task EnsureNameIsFree(BitweenDbContext dbContext, string name, int? except = null) + { + var wanted = name?.Trim().ToLower(); + if (string.IsNullOrEmpty(wanted)) return; + if (await dbContext.Set().AnyAsync(w => w.Id != except && w.Name.Trim().ToLower() == wanted)) + throw new SWValidationException("Name", $"There's already a work group named '{name.Trim()}'."); + } + private class Validate : AbstractValidator { public Validate() diff --git a/SW.Bitween.Api/Resources/WorkGroups/Delete.cs b/SW.Bitween.Api/Resources/WorkGroups/Delete.cs index 8b60641f..5d955352 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Delete.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Delete.cs @@ -25,7 +25,7 @@ public async Task Handle(int key, DeleteWorkGroupModel _) if (await dbContext.Set().AnyAsync(i => i.WorkGroupId.Value == category.Id)) throw new SWValidationException("CANT_BE_DELETED", "Workgroup with Subscriptions cant be deleted"); - // Data source statements run on a work group too, and that foreign key restricts as well — + // Data source statements run on a work group too, and that foreign key restricts as well - // left unchecked it refused the delete as a bare 500 naming the constraint. var statements = await dbContext.Set() .Where(s => s.WorkGroupId == key) diff --git a/SW.Bitween.Api/Resources/WorkGroups/Update.cs b/SW.Bitween.Api/Resources/WorkGroups/Update.cs index 531c7bae..9ebf6e1e 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Update.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Update.cs @@ -18,8 +18,10 @@ public async Task Handle(int key, CreateWorkGroupModel request) throw new SWValidationException("WORK_GROUP_NOT_FOUND", $"Category with id {key} was not found"); // Update binds the same CreateWorkGroupModel type as Create, so Create's - // Validate (IValidator) already runs for this request too — + // Validate (IValidator) already runs for this request too - // CqApiController resolves validators by the request's concrete type. + if (!string.Equals(workGroup.Name?.Trim(), request.Name?.Trim(), System.StringComparison.OrdinalIgnoreCase)) + await Create.EnsureNameIsFree(dbContext, request.Name, key); var oldBusMessageName = workGroup.GetBusMessageName(); workGroup.Name = request.Name; workGroup.BusMessageName = request.BusMessageName; diff --git a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs index 68b1ca52..547a9e62 100644 --- a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Resources.Xchanges { /// - /// Retries a selection of exchanges — either a hand-picked list of ids or everything a filter + /// Retries a selection of exchanges - either a hand-picked list of ids or everything a filter /// matches. Returns what it did: answers the same question /// beforehand, so the caller can show it and be sure the two agree. /// @@ -72,7 +72,7 @@ public async Task Handle(XchangeBulkRetry request) } // The tolerant read, not GetFile: a retry re-sends the original input, so an - // exchange whose input has been deleted or expired cannot be retried — and one of + // exchange whose input has been deleted or expired cannot be retried - and one of // those in a selection of five hundred must not take the other 499 with it. var xchangeFile = await _xchangeService.ReadInputFile(xchange); if (xchangeFile == null) @@ -106,7 +106,7 @@ await _xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGrou e.Validations.Any(v => v.Key == "ALREADY_RETRIED")) { // The planner resolves every selection to the end of its chain, so this is not - // reachable by choosing badly — it means someone retried this attempt in the + // reachable by choosing badly - it means someone retried this attempt in the // moment between the plan being worked out and it being carried out. Reported // like the other per-exchange refusals rather than thrown, so one racing // operator cannot cancel another's whole recovery. @@ -121,6 +121,9 @@ await _xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGrou retried++; } + if (retried > 0) + _dbContext.Record(_requestContext, OperatorAction.BulkRetry, $"{retried} exchanges", + $"{retried} retried, {plan.Skipped?.Count ?? 0} skipped"); await _dbContext.SaveChangesAsync(); plan.WillRetry = retried; diff --git a/SW.Bitween.Api/Resources/Xchanges/BulkRetryPlanner.cs b/SW.Bitween.Api/Resources/Xchanges/BulkRetryPlanner.cs index da8a9f5f..ec1064d4 100644 --- a/SW.Bitween.Api/Resources/Xchanges/BulkRetryPlanner.cs +++ b/SW.Bitween.Api/Resources/Xchanges/BulkRetryPlanner.cs @@ -14,14 +14,14 @@ namespace SW.Bitween.Resources.Xchanges; /// /// /// Shared by the preview and the retry itself so that what someone confirms is what runs. Built by -/// hand rather than injected, like RetryGroupBudget — it is a piece of one request's work, +/// hand rather than injected, like RetryGroupBudget - it is a piece of one request's work, /// not a service. /// internal sealed class BulkRetryPlanner { /// /// The largest selection one request will carry out. Every exchange retried means reading its - /// input back out of storage and writing a new exchange, in sequence, inside the one request — + /// input back out of storage and writing a new exchange, in sequence, inside the one request - /// so the ceiling is about what can finish before something upstream gives up waiting, not /// about the database. Past it the caller is asked to narrow the filter. /// @@ -56,7 +56,7 @@ internal async Task Prepare(XchangeBulkRetry request) { // A hand-picked list is already counted. A filter's selection was cut off at // Limit + 1 to notice it was too long without reading it all, so that one is - // counted properly now — the caller is told how far past the line it is rather + // counted properly now - the caller is told how far past the line it is rather // than just "501", and never a count of rows it did not ask about. Selected = string.IsNullOrWhiteSpace(request.Filter) ? selected.Count @@ -91,7 +91,7 @@ internal async Task Prepare(XchangeBulkRetry request) continue; } - // Two selections in the same chain — an exchange and its own retry, say — come to the + // Two selections in the same chain - an exchange and its own retry, say - come to the // same attempt, which is retried once. if (!targets.Contains(targetId)) targets.Add(targetId); @@ -117,7 +117,7 @@ private static string WhyNot(SelectionState state, bool substituted, bool reset) // Only when the caller asked for the subscription's current configuration, which there is // no way to read once the subscription is gone. Checked here as well as at the moment of // retrying, because a plan that promises to retry something the retry then skips makes the - // confirmation worthless — the whole point of showing it is that it is what will happen. + // confirmation worthless - the whole point of showing it is that it is what will happen. if (reset && state.SubscriptionMissing) return $"{subject} cannot have its properties re-resolved: the subscription no longer exists."; @@ -125,8 +125,8 @@ private static string WhyNot(SelectionState state, bool substituted, bool reset) return $"{subject} already has an auto-retry scheduled. Run that now instead."; // An exchange with no result is deliberately not skipped as "still running". That is what - // a broker outage leaves behind — hundreds of exchanges that were never processed and never - // will be — and retrying those in bulk is the main thing a wide selection is for. Refusing + // a broker outage leaves behind - hundreds of exchanges that were never processed and never + // will be - and retrying those in bulk is the main thing a wide selection is for. Refusing // them would take the recovery path away to protect against double-running work that, in // the case anyone actually selects in bulk, is not running at all. @@ -141,7 +141,7 @@ private static string WhyNot(SelectionState state, bool substituted, bool reset) /// would actually run, since the selected one is refused a second retry. /// /// - /// One query per round for the whole selection at once, rather than a walk per exchange — + /// One query per round for the whole selection at once, rather than a walk per exchange - /// five hundred selections down a chain of four is four queries, not two thousand. /// private async Task> FindNewestAttempts(List selected) @@ -150,8 +150,8 @@ private async Task> FindNewestAttempts(List s for (var depth = 0; depth < MaxDepth; depth++) { - // Where each selection has got to so far. Asking about that frontier — rather than - // about one level of the tree, tracking which nodes the walk had already seen — is what + // Where each selection has got to so far. Asking about that frontier - rather than + // about one level of the tree, tracking which nodes the walk had already seen - is what // keeps selections that sit in the same chain consistent with each other. With a seen // set, a selection lagging behind another stopped dead on a node the leader had already // stepped over, and was reported as "already retried, its newest attempt runs instead" @@ -168,7 +168,7 @@ private async Task> FindNewestAttempts(List s // One retry per exchange is the rule, but exchanges retried before it was enforced can // have more than one. Follow the newest, and let the retry tree in the drawer be where - // the fork is shown — a bulk retry has to choose something, and the newest attempt is + // the fork is shown - a bulk retry has to choose something, and the newest attempt is // the one that reflects where the work actually got to. var step = children .GroupBy(c => c.RetryFor) @@ -192,7 +192,7 @@ private async Task> FindNewestAttempts(List s /// /// What the payload promoted, for each exchange the plan names on either side of a - /// substitution or in a skip — so the caller can describe them the way the exchange list does + /// substitution or in a skip - so the caller can describe them the way the exchange list does /// rather than by id alone. /// private async Task>> ReadPromotedProperties( @@ -227,7 +227,7 @@ private sealed class SelectionState public System.DateTime? ScheduledRetryOn { get; set; } /// - /// Covers an exchange that never had a subscription — a document-only one — as well as one + /// Covers an exchange that never had a subscription - a document-only one - as well as one /// whose subscription has since been deleted. Both leave a reset with nothing to read. /// public bool SubscriptionMissing { get; set; } diff --git a/SW.Bitween.Api/Resources/Xchanges/Retry.cs b/SW.Bitween.Api/Resources/Xchanges/Retry.cs index 7b03b329..81abc361 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Retry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Retry.cs @@ -29,7 +29,8 @@ public async Task Handle(string key, XchangeRetry xchangeRetry) throw new SWValidationException("AUTO_RETRY_SCHEDULED", "An auto-retry is already scheduled for this exchange. Use \"Run Now\" to execute it immediately instead of retrying manually."); - var xchange = await dbContext.FindAsync(key); + var xchange = await dbContext.FindAsync(key) + ?? throw new SWNotFoundException(key.ToString()); var inputFileData = await xchangeService.GetFile(xchange, XchangeFileType.Input); var xchangeFile = new XchangeFile(inputFileData, xchange.InputName); var subscription = await dbContext.Subscriptions().FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId); @@ -46,10 +47,10 @@ public async Task Handle(string key, XchangeRetry xchangeRetry) await xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup, manualRetry: true); } - - + dbContext.Record(requestContext, OperatorAction.Retry, key, + xchangeRetry.Reset ? "with the subscription's current settings" : null); await dbContext.SaveChangesAsync(); - + return null; } } diff --git a/SW.Bitween.Api/Resources/Xchanges/RetryTree.cs b/SW.Bitween.Api/Resources/Xchanges/RetryTree.cs index 2ef802e1..36ca75c5 100644 --- a/SW.Bitween.Api/Resources/Xchanges/RetryTree.cs +++ b/SW.Bitween.Api/Resources/Xchanges/RetryTree.cs @@ -15,13 +15,13 @@ namespace SW.Bitween.Resources.Xchanges; /// /// /// Its own endpoint rather than part of the exchange search, because a chain cannot be had from the -/// same query that lists exchanges — it takes a walk of unknown length, and the search is already +/// same query that lists exchanges - it takes a walk of unknown length, and the search is already /// the expensive one. Callers avoid asking altogether for the common case: and come back with every row, and /// an exchange with neither has no chain to fetch. /// /// -/// Walked iteratively, a level at a time, rather than as a recursive query — the three supported +/// Walked iteratively, a level at a time, rather than as a recursive query - the three supported /// databases would each need their own SQL for that, to save queries on a chain that a retry budget /// keeps to single digits in practice. Both walks are indexed lookups on RetryFor. /// @@ -31,7 +31,7 @@ public class RetryTree : IQueryHandler { /// /// How far the walk goes in each direction before giving up and saying so. Far past any real - /// chain — a retry policy's budget is the practical limit — so this exists to keep a cycle in + /// chain - a retry policy's budget is the practical limit - so this exists to keep a cycle in /// the data from becoming an endless loop, not to shorten honest answers. /// private const int MaxDepth = 100; @@ -40,7 +40,7 @@ public class RetryTree : IQueryHandler /// How many attempts one answer carries. Depth alone does not bound the work: an exchange /// retried many times over before the one-retry rule existed has all of those as direct /// children, and every one would be loaded and serialized. Far above any real chain, so - /// reaching it means the data is unusual — and the answer then says it is partial rather than + /// reaching it means the data is unusual - and the answer then says it is partial rather than /// quietly costing the database more the older the exchange is. /// private const int MaxNodes = 500; @@ -64,7 +64,7 @@ public async Task Handle(XchangeRetryTreeRequest request) var truncated = false; // Up to the original. Each step reads one pointer, so the exchange asked about does not - // have to be the newest attempt — opening any of them shows the same chain. + // have to be the newest attempt - opening any of them shows the same chain. var rootId = request.Id; var ancestors = new HashSet { rootId }; for (var depth = 0; depth < MaxDepth; depth++) @@ -75,7 +75,7 @@ public async Task Handle(XchangeRetryTreeRequest request) .FirstOrDefaultAsync(); // Also covers the exchange not existing at all: no row, so no parent, and the walk - // down then finds nothing either — an empty tree rather than a 404, because a caller + // down then finds nothing either - an empty tree rather than a 404, because a caller // asking about an exchange it is looking at wants the chain, not another error to // handle. if (parent == null || !ancestors.Add(parent)) diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index af8fe0a5..4efce1e4 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -17,7 +17,7 @@ public class Search(BitweenDbContext dbContext, XchangeService xchangeService, { /// /// Largest exact total the exchange search reports. Beyond it the response carries - /// CountCap + 1, meaning "more than this" — the client renders that as "10,000+". + /// CountCap + 1, meaning "more than this" - the client renders that as "10,000+". /// Kept in step with COUNT_CAP in ClientApp's ExchangesPage.tsx. /// internal const int CountCap = 10_000; @@ -92,7 +92,7 @@ from delayedRetry in drGroup.DefaultIfEmpty() // a left one: an exchange still running, or one that failed before it // produced a result, has no row there. Reading a non-nullable int off // that null threw "Nullable object must have a value" out of the whole - // query — one such exchange 500'd the entire list. + // query - one such exchange 500'd the entire list. InputFileSize = xchange.InputSize, OutputFileSize = result != null ? result.OutputSize : 0, ResponseFileSize = result != null ? result.ResponseSize : 0, @@ -101,7 +101,7 @@ from delayedRetry in drGroup.DefaultIfEmpty() // gateway/bus-route partner, or the subscription's own PartnerId as a // fallback there too) but the column was added later with no backfill, so // pre-migration xchanges have it null even when their subscription carries - // a direct PartnerId — fall back to that for those legacy rows. + // a direct PartnerId - fall back to that for those legacy rows. PartnerId = xchange.PartnerId ?? subscriber.PartnerId, ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null, RetryBlockedReason = result.RetryBlockedReason @@ -113,8 +113,8 @@ from delayedRetry in drGroup.DefaultIfEmpty() searchyRequest.Sorts, searchyRequest.PageSize, searchyRequest.PageIndex); // Only id and information type, so the lookup has nothing to hide from a caller without - // the view permission. It returned whole rows before — errors, promoted values, source - // values — to anyone signed in. Capped, because exchanges run to millions and a lookup + // the view permission. It returned whole rows before - errors, promoted values, source + // values - to anyone signed in. Capped, because exchanges run to millions and a lookup // with no page size would otherwise list every one of them. if (lookup) return await s.Take(LookupLimit).ToDictionaryAsync(k => k.Id, v => v.DocumentName); @@ -129,7 +129,7 @@ from delayedRetry in drGroup.DefaultIfEmpty() // Which of these have already been retried, so the client can tell a spent exchange // from a retryable one without asking about each row's chain. Asked separately rather // than as a subquery in the projection above, because TotalCount reuses that query and - // an exact count already costs more than fetching the rows does — this way neither + // an exact count already costs more than fetching the rows does - this way neither // plan changes. One indexed lookup over RetryFor for the page's worth of ids. if (r.Count > 0) { diff --git a/SW.Bitween.Api/Resources/Xchanges/Summary.cs b/SW.Bitween.Api/Resources/Xchanges/Summary.cs new file mode 100644 index 00000000..10a807cf --- /dev/null +++ b/SW.Bitween.Api/Resources/Xchanges/Summary.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Xchanges; + +/// +/// The exchange list's filter, summarised: how many exchanges are in each status, and, over a date +/// range, when they started. GET /api/xchanges/summary, with the same filter= +/// parameters the search takes. +/// +/// +/// +/// A status filter in the request is ignored. This is what the status tabs above the list show - +/// each status's count under every other filter - so applying the selected tab first would turn +/// the other three into zeros. +/// +/// +/// Filters through , the lean query bulk retry and +/// export select with, rather than the search's own projection: nothing here needs a file link or +/// a promoted value, and the search's projection builds those in C#, which cannot be grouped on. +/// Every special filter the search understands is applied the same way, so a count is never of a +/// different set than the list it sits over. +/// +/// +[HandlerName("summary")] +public class Summary(BitweenDbContext dbContext, RequestContext requestContext) : ISearchyHandler +{ + /// The status numbers reads, in the order they are counted. + private const string Processing = "0", Success = "1", BadResponse = "2", Failed = "3"; + + public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) + { + // The same read the list itself is: whoever may see the exchanges, or the dashboard that + // summarises them, may see how many there are. + await requestContext.EnsurePermission(dbContext, Model.Permissions.Exchanges.View, Model.Permissions.Dashboard.View); + + // Removed before anything else reads the request, from every condition rather than only + // the first the special filters look at: a status filter left in a later condition would + // reach Searchy as a column, and XchangeRow.StatusFilter is not one the database has. + foreach (var condition in searchyRequest.Conditions) + foreach (var statusFilter in condition.Filters.Where(f => f.Field == "StatusFilter").ToList()) + condition.Filters.Remove(statusFilter); + + var rows = await XchangeFilters.FilteredRowsAsync(dbContext, searchyRequest); + + // Read after FilteredRowsAsync, which is what moves the request's dates to UTC. The + // StartedOn filters are plain columns, so Searchy has applied them and left them in place. + var (rangeStart, rangeEnd) = StartedOnRange(searchyRequest); + + await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted); + + // One capped count per status rather than a single grouped count: each can stop at the + // cap, as the search's total does, where a grouping has to visit every matching row. Four + // short queries over a broad filter cost far less than one exhaustive one. + var counts = new XchangeStatusCounts + { + Processing = await CountCapped(rows, Processing), + Success = await CountCapped(rows, Success), + BadResponse = await CountCapped(rows, BadResponse), + Failed = await CountCapped(rows, Failed), + }; + + var summary = new XchangeSummary { Counts = counts }; + if (rangeStart is not { } start) + return summary; + + var end = rangeEnd ?? DateTime.UtcNow; + if (end < start) + return summary; + + var bucketMinutes = XchangeHistogram.ChooseBucketMinutes(start, end); + summary.BucketMinutes = bucketMinutes; + summary.Buckets = XchangeHistogram.Fold(start, end, bucketMinutes, + await StartedCounts(rows, bucketMinutes)); + return summary; + } + + private Task CountCapped(IQueryable rows, string status) => + rows.WhereStatus(status, dbContext).Take(Search.CountCap + 1).CountAsync(); + + /// + /// The matching exchanges grouped by when they started, to the precision the bucket size + /// needs, and by the two columns their status is made of. + /// + /// + /// + /// Unlike the counts, this cannot stop early: grouping visits every row in the range. What + /// keeps it affordable is that a histogram is only asked for over a date range, which the index + /// on StartedOn narrows to, and that what comes back is at most one row per minute per status + /// rather than one per exchange. + /// + /// + /// Grouped on the date's parts rather than on a computed bucket number, because Year, Month, + /// Day, Hour and Minute translate on every provider the solution supports, while date + /// arithmetic is spelt differently by each of them. The folding into buckets wider than the + /// grouping happens in . Status is grouped as its two + /// nullable columns, not as the four statuses, for the same reason: a CASE over them would be + /// one more thing to translate four ways. + /// + /// + private static async Task> StartedCounts(IQueryable rows, + int bucketMinutes) + { + if (bucketMinutes >= 1440) + return await rows + .GroupBy(r => new { r.StartedOn.Year, r.StartedOn.Month, r.StartedOn.Day, r.Status, r.ResponseBad }) + .Select(g => new XchangeStartedCount + { + Year = g.Key.Year, + Month = g.Key.Month, + Day = g.Key.Day, + Status = g.Key.Status, + ResponseBad = g.Key.ResponseBad, + Count = g.Count() + }) + .ToListAsync(); + + if (bucketMinutes >= 60) + return await rows + .GroupBy(r => new + { + r.StartedOn.Year, r.StartedOn.Month, r.StartedOn.Day, r.StartedOn.Hour, r.Status, + r.ResponseBad + }) + .Select(g => new XchangeStartedCount + { + Year = g.Key.Year, + Month = g.Key.Month, + Day = g.Key.Day, + Hour = g.Key.Hour, + Status = g.Key.Status, + ResponseBad = g.Key.ResponseBad, + Count = g.Count() + }) + .ToListAsync(); + + return await rows + .GroupBy(r => new + { + r.StartedOn.Year, r.StartedOn.Month, r.StartedOn.Day, r.StartedOn.Hour, r.StartedOn.Minute, + r.Status, r.ResponseBad + }) + .Select(g => new XchangeStartedCount + { + Year = g.Key.Year, + Month = g.Key.Month, + Day = g.Key.Day, + Hour = g.Key.Hour, + Minute = g.Key.Minute, + Status = g.Key.Status, + ResponseBad = g.Key.ResponseBad, + Count = g.Count() + }) + .ToListAsync(); + } + + /// + /// The StartedOn range the request's filters give, as the list sends it: a "from" as + /// StartedOn:6: (greater than or equal) and a "to" as StartedOn:8: (less than or + /// equal). Either may be missing. Two of the same kind narrow it to their overlap, as the + /// filters themselves do. + /// + internal static (DateTime? Start, DateTime? End) StartedOnRange(SearchyRequest searchyRequest) + { + DateTime? start = null, end = null; + foreach (var filter in searchyRequest.Conditions.SelectMany(c => c.Filters) + .Where(f => f.Field == nameof(XchangeRow.StartedOn))) + { + // DatesToUtc sets this only for a value that reads as a date; anything else was never a + // range Searchy could have applied either. + if (filter.ValueDateTime is not { } dateTime) + continue; + + var value = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + if (filter.Rule == SearchyRule.GreaterThanOrEquals) + start = start is { } s && s > value ? s : value; + else if (filter.Rule == SearchyRule.LessThanOrEquals) + end = end is { } e && e < value ? e : value; + } + + return (start, end); + } +} diff --git a/SW.Bitween.Api/Resources/Xchanges/Update.cs b/SW.Bitween.Api/Resources/Xchanges/Update.cs index f4ad5001..234cc2a0 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Update.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Update.cs @@ -15,14 +15,19 @@ namespace SW.Bitween.Resources.Xchanges { [Unprotect] public class Update(RequestContext requestContext, XchangeService xchangeService, BitweenDbContext dbContext, - BitweenOptions BitweenSettings, IInfolinkCache cache) : ICommandHandler + BitweenOptions BitweenSettings, IInfolinkCache cache, + Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor = null) : ICommandHandler { public async Task Handle(string documentIdOrName, dynamic request) { Document document; - //Inject external request context values into the object - request._ExternalRequestContext = JsonConvert.SerializeObject(requestContext.Values); + // Inject external request context values into the object - minus the caller's + // credentials. This lands in the stored input file, where anyone who can view + // exchanges reads it, and is handed to mappers and handlers, none of which need the + // partner's key to do their job. + request._ExternalRequestContext = JsonConvert.SerializeObject( + requestContext.Values.Where(v => !IsCredential(v.Name, BitweenSettings.PartnerKeyHeader)).ToList()); if (int.TryParse(documentIdOrName, out var documentId)) document = await cache.DocumentByIdAsync(documentId); @@ -44,6 +49,11 @@ public async Task Handle(string documentIdOrName, dynamic request) var sub = subs.SingleOrDefault(); + string body = request.ToString(); + var schemaErrors = await DocumentSchema.Check(document.DocumentFormat, document.ValidationSchema, body); + if (schemaErrors.Count > 0) + throw new SWValidationException("SCHEMA_MISMATCH", string.Join("\n", schemaErrors)); + if (par.Partner.Id == Partner.SystemId && sub is null) { await xchangeService.SubmitFilterXchange(document.Id,new XchangeFile(request.ToString())); @@ -59,10 +69,11 @@ public async Task Handle(string documentIdOrName, dynamic request) .Where(item => item.Name.ToLower() == "waitresponse") .Select(item => item.Value).FirstOrDefault(); + // No header, no wait - as before. With one, the wait is capped; see ResultWait. var waitResponse = 0; if (int.TryParse(waitResponseHeader, out var waitResponseValue)) { - waitResponse = waitResponseValue <= 0 ? 120 : waitResponseValue; + waitResponse = ResultWait.Clamp(waitResponseValue, BitweenSettings.MaxResponseWaitSeconds); xchangeReferences.Add($"waitresponse: {waitResponse}"); } @@ -70,10 +81,39 @@ public async Task Handle(string documentIdOrName, dynamic request) var globalAdapterValuesSets = await cache.ListGlobalAdapterValuesSetsAsync(); var validatorProperties = sub.ValidatorProperties.ToDictionary().Fill(par.Partner, globalAdapterValuesSets); - await xchangeService.RunValidator(sub.ValidatorId, validatorProperties, xchangeFile); - - var xchangeId = - await xchangeService.SubmitSubscriptionXchange(sub.Id, xchangeFile, xchangeReferences.ToArray()); + await xchangeService.RunValidator(sub.ValidatorRef, validatorProperties, xchangeFile); + + // Same Idempotency-Key from the same partner for the same type: the first call's exchange. + var idempotencyKey = requestContext.Values + .Where(v => v.Type == RequestValueType.HttpHeader && + v.Name.Equals(IdempotencyGuard.Header, StringComparison.OrdinalIgnoreCase)) + .Select(v => v.Value?.Trim()).FirstOrDefault(); + if (idempotencyKey?.Length > IdempotencyGuard.MaxKeyLength) + throw new SWValidationException("IDEMPOTENCY_KEY", + $"{IdempotencyGuard.Header} is longer than {IdempotencyGuard.MaxKeyLength} characters."); + var idempotencyScope = $"legacy:partner:{par.Partner.Id}:document:{document.Id}"; + + var xchangeId = string.IsNullOrEmpty(idempotencyKey) + ? null + : await IdempotencyGuard.FindAsync(dbContext, idempotencyScope, idempotencyKey); + if (xchangeId is null) + { + var subscription = await cache.SubscriptionByIdAsync(sub.Id); + var xchange = await xchangeService.CreateXchange(subscription, xchangeFile, + xchangeReferences.ToArray(), Guid.NewGuid().ToString("N")); + if (!string.IsNullOrEmpty(idempotencyKey)) + await IdempotencyGuard.StageAsync(dbContext, idempotencyScope, idempotencyKey, xchange.Id); + try + { + await dbContext.SaveChangesAsync(); + xchangeId = xchange.Id; + } + catch (DbUpdateException ex) when (IdempotencyGuard.IsDuplicateKey(ex)) + { + dbContext.ChangeTracker.Clear(); + xchangeId = await IdempotencyGuard.FindAsync(dbContext, idempotencyScope, idempotencyKey); + } + } if (waitResponse <= 0) return new CqApiResult(xchangeId) @@ -81,15 +121,11 @@ public async Task Handle(string documentIdOrName, dynamic request) Status = CqApiResultStatus.Ok }; - var currentFibTerm = 1; - var previousTerm = 1; - while (currentFibTerm <= waitResponse) + var available = await ResultWait.UntilAsync(() => IsResultAvailable(xchangeId), waitResponse, + httpContextAccessor?.HttpContext?.RequestAborted ?? System.Threading.CancellationToken.None); + + if (available) { - await Task.Delay(TimeSpan.FromSeconds(currentFibTerm)); - var nextTerm = Math.Min(currentFibTerm + previousTerm, 8); - previousTerm = currentFibTerm; - currentFibTerm = nextTerm; - if (!await IsResultAvailable(xchangeId)) continue; var xchangeResult = await dbContext.FindAsync(xchangeId); @@ -130,5 +166,16 @@ private async Task IsResultAvailable(string xchangeId) .AsNoTracking() .AnyAsync(i => i.Id == xchangeId); } + + private static readonly HashSet CredentialNames = new(StringComparer.OrdinalIgnoreCase) + { + "authorization", "proxy-authorization", "cookie", PartnerKeyHeaders.Default, + }; + + private static bool IsCredential(string name, string partnerKeyHeader) => + name is not null && + (CredentialNames.Contains(name) || + (!string.IsNullOrWhiteSpace(partnerKeyHeader) && + name.Equals(partnerKeyHeader.Trim(), StringComparison.OrdinalIgnoreCase))); } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/Xchanges/XchangeFilters.cs b/SW.Bitween.Api/Resources/Xchanges/XchangeFilters.cs index 862a84d0..7043cc3f 100644 --- a/SW.Bitween.Api/Resources/Xchanges/XchangeFilters.cs +++ b/SW.Bitween.Api/Resources/Xchanges/XchangeFilters.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using SW.EfCoreExtensions; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -20,6 +21,69 @@ namespace SW.Bitween.Resources.Xchanges; /// internal static class XchangeFilters { + /// + /// The exchanges a filter matches, filtered exactly as the search would have filtered them, as + /// lean rows: the columns the list can filter on and nothing else. + /// + /// + /// + /// What a "select all matching" bulk action and the summary both start from. Dates are moved to + /// UTC and a run filter is resolved here, inside the one entry point, so neither caller can + /// forget a step and select a different set than the list showed for the same filter. + /// + /// + /// The projection carries the columns the exchange list can filter on (see + /// buildExchangeQuery in the client) and nothing else, so this stays translatable and + /// composable - the search's own projection cannot be reused for that, as it builds file URLs + /// in C#. Filtering on a column that is not here would silently match nothing, so a new filter + /// on the list needs a column here too. + /// + /// + /// The special filters are removed from as they are applied, + /// as they are for the search; what is left in it afterwards is only what Searchy handled. + /// + /// + internal static async Task> FilteredRowsAsync(BitweenDbContext dbContext, + SearchyRequest searchyRequest) + { + searchyRequest.DatesToUtc(); + // Async, and inside here rather than in the callers, so a "select all matching" over a run + // selects exactly the rows the list showed for it. + await ResolveReceiveAttemptFilterAsync(searchyRequest, dbContext); + + var query = from xchange in dbContext.Set() + join result in dbContext.Set() on xchange.Id equals result.Id into xr + from result in xr.DefaultIfEmpty() + join agg in dbContext.Set() on xchange.Id equals agg.Id into xa + from agg in xa.DefaultIfEmpty() + join promoted in dbContext.Set() on xchange.Id equals promoted.Id into xp + from promoted in xp.DefaultIfEmpty() + join subscriber in dbContext.Set() on xchange.SubscriptionId equals subscriber.Id into xs + from subscriber in xs.DefaultIfEmpty() + select new XchangeRow + { + Id = xchange.Id, + SubscriptionId = xchange.SubscriptionId, + DocumentId = xchange.DocumentId, + StartedOn = xchange.StartedOn, + CorrelationId = xchange.CorrelationId, + Status = result.Success, + ResponseBad = result.ResponseBad, + // For the ErrorContains filter. Only read when that filter is set, so it + // costs nothing to the selections that do not use it. + Exception = result.Exception, + RetryFor = xchange.RetryFor, + AggregationXchangeId = agg.AggregationXchangeId, + PromotedPropertiesRaw = promoted.PropertiesRaw, + // Same fallback the search makes for exchanges written before the column + // existed, so a partner filter selects the same rows it listed. + PartnerId = xchange.PartnerId ?? subscriber.PartnerId + }; + + query = query.ApplySpecialFilters(searchyRequest, dbContext); + return query.AsNoTracking().Search(searchyRequest.Conditions); + } + /// /// Turns a ReceiveAttemptId filter into the Id filter the rest of the pipeline /// already understands, by reading the run's own record of the exchanges it created. @@ -28,11 +92,11 @@ internal static class XchangeFilters /// Separate from , and async, because the run has to be read /// before the query can be built. ReceiveAttempt.ExchangeIds is persisted as a single /// separator-delimited string rather than an array (see BitweenDbContext), so no - /// subquery can reach inside it — and with three database providers in the solution there is + /// subquery can reach inside it - and with three database providers in the solution there is /// no one translation that would. /// /// It rewrites rather than filters here so the result stays identical to the long - /// ?ids=a,b,c URL this replaces, relatives and all — the only thing that changes is that + /// ?ids=a,b,c URL this replaces, relatives and all - the only thing that changes is that /// the ids no longer have to fit in a web address. /// internal static async Task ResolveReceiveAttemptFilterAsync(SearchyRequest searchyRequest, @@ -59,7 +123,7 @@ internal static async Task ResolveReceiveAttemptFilterAsync(SearchyRequest searc $"Run {attemptId} no longer exists. Its history may have been cleaned up."); // A run that created nothing leaves an empty array, and the Contains branch below reads - // that as "matches no row" — which is the honest answer. Dropping the filter instead + // that as "matches no row" - which is the honest answer. Dropping the filter instead // would widen the selection to every exchange, and bulk retry acts on whatever this // selects. condition.Filters.Add(new SearchyFilter @@ -115,46 +179,12 @@ internal static IQueryable ApplySpecialFilters(this IQueryable f.Field == "StatusFilter").ToList(); foreach (var statusFilter in statusFilters) { - switch (statusFilter.Value) - { - case "0": - // "Still running" means no result row exists yet. Asking for it as - // Status == null reads as `x0.success IS NULL` on the left join, and - // Postgres cannot estimate that: it guesses one row, plans every join - // above it for one row, and picks per-row sequential scans of the small - // side tables. Measured on 1M exchanges that was 22.8s for 25 rows. - // NOT EXISTS asks the same question as an anti-join, which it can - // estimate — 34ms. Equivalent because success is NOT NULL, so a result - // row can never itself carry a null status. - query = query.Where(i => - !dbContext.Set().Any(r => r.Id == i.Id)); - break; - case "1": - query = query.Where(i => i.Status == true && i.ResponseBad != true); - break; - - case "2": - query = query.Where(i => i.Status == true && i.ResponseBad == true); - break; - - case "3": - query = query.Where(i => i.Status == false); - break; - - default: - // The filter is removed below whether or not it matched, so falling through - // here used to drop it silently and widen the selection to everything. A - // search returning too much is merely wrong; bulk retry runs over whatever - // this selects, so an unreadable status has to be refused rather than ignored. - throw new SWValidationException("NOT_SUPPORTED", - $"'{statusFilter.Value}' is not an exchange status."); - } - + query = query.WhereStatus(statusFilter.Value, dbContext); condition.Filters.Remove(statusFilter); } // "Only the newest attempt of each chain." An exchange that has been retried is superseded - // by that retry, so a list of failures otherwise counts one piece of work as many rows — + // by that retry, so a list of failures otherwise counts one piece of work as many rows - // a chain retried nine times fills nine of them, none of which is the current state. // // NOT EXISTS rather than a left join for the same reason the still-running status uses it: @@ -162,12 +192,67 @@ internal static IQueryable ApplySpecialFilters(this IQueryable f.Field == "LatestOnly").ToList(); foreach (var latestFilter in latestFilters) { - if (latestFilter.Value?.ToString()?.ToLower() is "true" or "1") + if (IsOn(latestFilter)) query = query.Where(i => !dbContext.Set().Any(r => r.RetryFor == i.Id)); condition.Filters.Remove(latestFilter); } + // "Only retries": attempts made from an earlier exchange, whether a person asked for them or + // a retry policy did. RetryFor is on the exchange itself, so no second table is involved. + var retryFilters = condition.Filters.Where(f => f.Field == "IsRetry").ToList(); + foreach (var retryFilter in retryFilters) + { + if (IsOn(retryFilter)) + query = query.Where(i => i.RetryFor != null); + + condition.Filters.Remove(retryFilter); + } + + // "Waiting for a scheduled retry": a DelayedRetry row exists for it. That row is keyed by the + // exchange's own id and deleted when the retry runs, so its presence is the whole question. + // EXISTS rather than reading the search's left join to DelayedRetry, for the same reason + // the still-running status uses NOT EXISTS - and because the lean projection bulk actions + // and the summary filter over does not join DelayedRetry at all. + var scheduledFilters = condition.Filters.Where(f => f.Field == "HasScheduledRetry").ToList(); + foreach (var scheduledFilter in scheduledFilters) + { + if (IsOn(scheduledFilter)) + query = query.Where(i => dbContext.Set().Any(d => d.Id == i.Id)); + + condition.Filters.Remove(scheduledFilter); + } + + // "Rolled up into an aggregation": the exchange was taken into an aggregation exchange, + // which is what carries its data onwards from there. + var aggregatedFilters = condition.Filters.Where(f => f.Field == "Aggregated").ToList(); + foreach (var aggregatedFilter in aggregatedFilters) + { + if (IsOn(aggregatedFilter)) + query = query.Where(i => i.AggregationXchangeId != null); + + condition.Filters.Remove(aggregatedFilter); + } + + // Text inside the error, case-insensitive. Named apart from the Exception column rather than + // reusing it, because a plain Searchy filter on Exception would compare case as stored, and + // an error message's case is whatever the adapter that threw it chose. Folded on both sides + // like the promoted properties below; a leading-wildcard LIKE could use no index either way. + // An empty value is no filter at all rather than "has any error", which is what Contains("") + // would quietly turn into. + var errorFilters = condition.Filters.Where(f => f.Field == "ErrorContains").ToList(); + foreach (var errorFilter in errorFilters) + { + var value = errorFilter.Value?.ToString(); + if (!string.IsNullOrEmpty(value)) + { + value = value.ToLower(); + query = query.Where(i => i.Exception.ToLower().Contains(value)); + } + + condition.Filters.Remove(errorFilter); + } + // The one promoted property the value below has to be found in. Sent apart from the value // because the client used to send them together as "key:value", which only matched a value // that starts with what was typed: "SO-1001" was found by "SO", never by "1001". @@ -215,4 +300,56 @@ where rest.Substring(0, rest.IndexOf(",")).IndexOf(value) >= 0 return query; } + + /// + /// Whether an on/off filter is on. Only "true" or "1" turn one on; anything else leaves it off + /// rather than refusing it, which is what LatestOnly has always done - "select all + /// matching" with no filters set sends LatestOnly:1:false to mean "everything". + /// + private static bool IsOn(SearchyFilter filter) => + filter.Value?.ToString()?.ToLower() is "true" or "1"; + + /// + /// Narrows to one exchange status, by the number the client's + /// StatusFilter sends: 0 still running, 1 success, 2 success with a bad response, 3 failed. + /// + /// + /// Its own method so the summary can count each status under the same definition the list + /// filters by. A count that defined "success" even slightly differently from the tab it sits on + /// would show one number and list another. + /// + internal static IQueryable WhereStatus(this IQueryable query, object status, + BitweenDbContext dbContext) + { + switch (status) + { + case "0": + // "Still running" means no result row exists yet. Asking for it as + // Status == null reads as `x0.success IS NULL` on the left join, and + // Postgres cannot estimate that: it guesses one row, plans every join + // above it for one row, and picks per-row sequential scans of the small + // side tables. Measured on 1M exchanges that was 22.8s for 25 rows. + // NOT EXISTS asks the same question as an anti-join, which it can + // estimate - 34ms. Equivalent because success is NOT NULL, so a result + // row can never itself carry a null status. + return query.Where(i => + !dbContext.Set().Any(r => r.Id == i.Id)); + case "1": + return query.Where(i => i.Status == true && i.ResponseBad != true); + + case "2": + return query.Where(i => i.Status == true && i.ResponseBad == true); + + case "3": + return query.Where(i => i.Status == false); + + default: + // The filter is removed by the caller whether or not it matched, so falling through + // here used to drop it silently and widen the selection to everything. A search + // returning too much is merely wrong; bulk retry runs over whatever this selects, + // so an unreadable status has to be refused rather than ignored. + throw new SWValidationException("NOT_SUPPORTED", + $"'{status}' is not an exchange status."); + } + } } diff --git a/SW.Bitween.Api/Resources/Xchanges/XchangeHistogram.cs b/SW.Bitween.Api/Resources/Xchanges/XchangeHistogram.cs new file mode 100644 index 00000000..031e2ffb --- /dev/null +++ b/SW.Bitween.Api/Resources/Xchanges/XchangeHistogram.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SW.Bitween.Model; + +namespace SW.Bitween.Resources.Xchanges; + +/// +/// How many exchanges started in one slice of time with one status combination, as the summary's +/// grouping query returns it: the date split into parts, because the parts are what every database +/// provider can group on. +/// +/// +/// The parts finer than the grouping asked for are left 0 - a by-day grouping has no hour. +/// +public sealed class XchangeStartedCount +{ + public int Year { get; set; } + public int Month { get; set; } + public int Day { get; set; } + public int Hour { get; set; } + public int Minute { get; set; } + + /// The result's success; null when there is no result yet. + public bool? Status { get; set; } + + public bool? ResponseBad { get; set; } + public int Count { get; set; } +} + +/// +/// The pure half of the summary's histogram: picking a bucket size for a range, and folding the +/// grouped counts into contiguous, zero-filled buckets. No database here, so it can be tested on +/// its own. +/// +public static class XchangeHistogram +{ + /// + /// The bucket sizes a histogram may use, in minutes, smallest first. Each divides a day or is a + /// whole number of days, so buckets line up with the clock: 5-minute buckets start on :00, :05, + /// and so on, never at whatever minute the range happened to start. + /// + public static readonly IReadOnlyList BucketSizes = + [1, 5, 10, 15, 30, 60, 120, 180, 360, 720, 1440, 10080]; + + /// The most buckets a range is divided into, unless even the largest size gives more. + public const int MaxBuckets = 60; + + /// + /// The smallest bucket size that divides the range into at most + /// buckets. A day comes out as 49 half-hours, a week as 57 three-hour buckets, thirty days as 31 + /// days. Fewer is possible when the next size down was only just too fine: the last hour is 61 + /// one-minute buckets once both of its ends are counted, so it gets 13 five-minute ones instead. + /// + /// + /// A range longer than sixty weeks gets weekly buckets anyway, more than sixty of them: a week + /// is the coarsest size there is, and inventing a longer one would no longer line up with + /// anything on a calendar. + /// + public static int ChooseBucketMinutes(DateTime start, DateTime end) + { + foreach (var size in BucketSizes) + if (BucketCount(start, end, size) <= MaxBuckets) + return size; + + return BucketSizes[^1]; + } + + /// + /// How many buckets of cover the range, counting both the one + /// falls in and the one falls in - the end is + /// inclusive, as the list's "to" date is. + /// + public static long BucketCount(DateTime start, DateTime end, int bucketMinutes) + { + if (end < start) + return 0; + + var ticks = BucketTicks(bucketMinutes); + return (Floor(end, bucketMinutes).Ticks - Floor(start, bucketMinutes).Ticks) / ticks + 1; + } + + /// + /// The start of the bucket falls in, in UTC. + /// + /// + /// Counted from , which is midnight, so every size that divides a + /// day floors to a clock boundary; and a Monday, so weekly buckets start on Mondays. + /// + public static DateTime Floor(DateTime time, int bucketMinutes) + { + var ticks = BucketTicks(bucketMinutes); + return new DateTime(time.Ticks - time.Ticks % ticks, DateTimeKind.Utc); + } + + /// + /// Folds the grouped counts into buckets of , from the one + /// falls in to the one falls in, every bucket + /// present whether anything started in it or not - a gap in the list would read as a gap in + /// time to anything drawing it. + /// + /// + /// The status is read from the two columns the way + /// reads it, so the bars add up to the counts beside them: no result is still running (the + /// result's success is never null, so a null here means the row is missing), a success with a + /// bad response is its own status, and any other success is a success. A count that falls + /// outside the range is dropped rather than stretched onto the nearest bucket. + /// + public static List Fold(DateTime start, DateTime end, int bucketMinutes, + IEnumerable counts) + { + var buckets = new List(); + if (end < start) + return buckets; + + var first = Floor(start, bucketMinutes); + var count = BucketCount(start, end, bucketMinutes); + var size = TimeSpan.FromMinutes(bucketMinutes); + for (var i = 0; i < count; i++) + buckets.Add(new XchangeSummaryBucket { Start = first + size * i }); + + var ticks = BucketTicks(bucketMinutes); + foreach (var c in counts) + { + var started = new DateTime(c.Year, c.Month, c.Day, c.Hour, c.Minute, 0, DateTimeKind.Utc); + var index = (Floor(started, bucketMinutes).Ticks - first.Ticks) / ticks; + if (index < 0 || index >= buckets.Count) + continue; + + var bucket = buckets[(int)index]; + if (c.Status == null) + bucket.Processing += c.Count; + else if (c.Status == false) + bucket.Failed += c.Count; + else if (c.ResponseBad == true) + bucket.BadResponse += c.Count; + else + bucket.Success += c.Count; + } + + return buckets; + } + + private static long BucketTicks(int bucketMinutes) => bucketMinutes * TimeSpan.TicksPerMinute; +} diff --git a/SW.Bitween.Api/Resources/Xchanges/XchangeSelection.cs b/SW.Bitween.Api/Resources/Xchanges/XchangeSelection.cs index 4e6a56cc..ef1335d8 100644 --- a/SW.Bitween.Api/Resources/Xchanges/XchangeSelection.cs +++ b/SW.Bitween.Api/Resources/Xchanges/XchangeSelection.cs @@ -2,15 +2,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using SW.EfCoreExtensions; -using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; namespace SW.Bitween.Resources.Xchanges; /// -/// The exchanges a bulk action on the exchange list covers — a bulk retry, a files export: the rows +/// The exchanges a bulk action on the exchange list covers - a bulk retry, a files export: the rows /// picked by hand, or everything a "select all matching" filter matches minus the rows unticked after. /// /// @@ -56,47 +54,10 @@ internal async Task Count(IXchangeSelection request) /// filtered them. /// /// - /// The projection carries the columns the exchange list can filter on (see - /// buildExchangeQuery in the client) and nothing else, so this stays translatable and - /// composable — the search's own projection cannot be reused for that, as it builds file URLs - /// in C#. Filtering on a column that is not here would silently match nothing, so a new filter - /// on the list needs a column here too. + /// The query itself lives in , shared with the + /// summary so the counts above the list and the set a bulk action acts on are the same rows - + /// read its remarks before filtering on a new column. /// - private async Task> Query(IXchangeSelection request) - { - var searchyRequest = new SearchyRequest(request.Filter); - searchyRequest.DatesToUtc(); - // Async, and inside here rather than in the two callers, so a "select all matching" over a - // run selects exactly the rows the list showed for it. - await XchangeFilters.ResolveReceiveAttemptFilterAsync(searchyRequest, dbContext); - - var query = from xchange in dbContext.Set() - join result in dbContext.Set() on xchange.Id equals result.Id into xr - from result in xr.DefaultIfEmpty() - join agg in dbContext.Set() on xchange.Id equals agg.Id into xa - from agg in xa.DefaultIfEmpty() - join promoted in dbContext.Set() on xchange.Id equals promoted.Id into xp - from promoted in xp.DefaultIfEmpty() - join subscriber in dbContext.Set() on xchange.SubscriptionId equals subscriber.Id into xs - from subscriber in xs.DefaultIfEmpty() - select new XchangeRow - { - Id = xchange.Id, - SubscriptionId = xchange.SubscriptionId, - DocumentId = xchange.DocumentId, - StartedOn = xchange.StartedOn, - CorrelationId = xchange.CorrelationId, - Status = result.Success, - ResponseBad = result.ResponseBad, - RetryFor = xchange.RetryFor, - AggregationXchangeId = agg.AggregationXchangeId, - PromotedPropertiesRaw = promoted.PropertiesRaw, - // Same fallback the search makes for exchanges written before the column - // existed, so a partner filter selects the same rows it listed. - PartnerId = xchange.PartnerId ?? subscriber.PartnerId - }; - - query = query.ApplySpecialFilters(searchyRequest, dbContext); - return query.AsNoTracking().Search(searchyRequest.Conditions); - } + private Task> Query(IXchangeSelection request) => + XchangeFilters.FilteredRowsAsync(dbContext, new SearchyRequest(request.Filter)); } diff --git a/SW.Bitween.Api/SW.Bitween.Api.csproj b/SW.Bitween.Api/SW.Bitween.Api.csproj index cdb3243e..5b9d36d7 100644 --- a/SW.Bitween.Api/SW.Bitween.Api.csproj +++ b/SW.Bitween.Api/SW.Bitween.Api.csproj @@ -6,7 +6,8 @@ - + + @@ -21,7 +22,7 @@ - + @@ -35,7 +36,7 @@ @@ -45,12 +46,20 @@ + + - + + + + + + diff --git a/SW.Bitween.Api/Services/AESCryptoService.cs b/SW.Bitween.Api/Services/AESCryptoService.cs index f9916092..aa5cf885 100644 --- a/SW.Bitween.Api/Services/AESCryptoService.cs +++ b/SW.Bitween.Api/Services/AESCryptoService.cs @@ -8,8 +8,8 @@ namespace SW.Bitween; public static class AESCryptoService { /// - /// The salt this has always used. It is a constant, and a well known one — the bytes spell - /// "Ivan Medvedev" from an old MSDN sample — so it adds nothing an attacker does not have. + /// The salt this has always used. It is a constant, and a well known one - the bytes spell + /// "Ivan Medvedev" from an old MSDN sample - so it adds nothing an attacker does not have. /// It cannot be changed without making every value already encrypted undecryptable, so it /// stays until there is a migration to move ciphertext to a per-value salt. /// diff --git a/SW.Bitween.Api/Services/AdapterRequirements.cs b/SW.Bitween.Api/Services/AdapterRequirements.cs index 5a4aa60f..589ebcd3 100644 --- a/SW.Bitween.Api/Services/AdapterRequirements.cs +++ b/SW.Bitween.Api/Services/AdapterRequirements.cs @@ -10,19 +10,58 @@ namespace SW.Bitween; /// Which of an adapter's required startup properties a caller failed to supply. /// /// The answer was written out four times across the subscription validators before this -/// existed — three in Update alone. Create needs the same answer, and six copies of it would +/// existed - three in Update alone. Create needs the same answer, and six copies of it would /// be six places for the rule to drift. Whether the adapter runs in-process or in a serverless /// container is 's problem, not this one's. /// /// -public class AdapterRequirements(AdapterStartupValues startupValues) +public class AdapterRequirements( + AdapterStartupValues startupValues, + Services.Adapters.AdapterCatalog catalog = null, + BitweenOptions bitweenOptions = null) { + /// + /// Refuses a version pin that cannot run: on a native adapter, which has no versions; on a + /// slot with no adapter; on a version that was never published or has been withdrawn. And + /// refuses an adapter - pinned or current - whose manifest asks for a newer Bitween than this + /// one, when the slot is being changed to it. Nothing is checked for a slot left as it was, so + /// saving an unrelated field never fails over a choice made before. + /// + public async Task EnsureRunnable(string slot, string adapterId, string version, bool changed) + { + if (!changed || string.IsNullOrWhiteSpace(adapterId)) return; + + if (!string.IsNullOrWhiteSpace(version)) + { + if (NativeAdapterDiscoveryService.IsNative(adapterId)) + throw new SWValidationException("ADAPTER_VERSION", + $"The {slot} '{adapterId}' is built in and has no versions to pin."); + if (catalog != null && !await catalog.HasVersionAsync(adapterId, version)) + throw new SWValidationException("ADAPTER_VERSION", + $"Version {version} of the {slot} '{adapterId}' is not published, or has been withdrawn."); + } + + if (catalog == null) return; + var manifest = await catalog.ManifestOf(SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Ref(adapterId, version)); + var needs = manifest?.Compatibility?.MinVersionOf(Services.Adapters.AdapterCatalog.Application); + if (!BitweenInfo.Satisfies(needs, bitweenOptions)) + throw new SWValidationException("ADAPTER_NEEDS_NEWER_BITWEEN", + $"The {slot} '{adapterId}'{(string.IsNullOrWhiteSpace(version) ? "" : $" {version}")} needs Bitween {needs} or later; this is {BitweenInfo.Version(bitweenOptions)}."); + } + /// Native (native: prefix) or serverless. Null/blank means nothing is missing. /// What the caller supplied. Blank values count as not supplied. public async Task> MissingFor(string adapterId, ICollection provided) { if (string.IsNullOrEmpty(adapterId)) return Array.Empty(); + // A version that can't be pinned has nothing to describe. Asking for it started the + // package to read its settings, which threw - a 500 from the request validator, which runs + // before EnsureRunnable can refuse the version with a reason. + var (id, version) = SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Split(adapterId); + if (version != null && catalog != null && !await catalog.HasVersionAsync(id, version)) + return Array.Empty(); + var required = (await startupValues.Describe(adapterId)) .Where(p => !p.Value.Optional).Select(p => p.Key); diff --git a/SW.Bitween.Api/Services/AdapterSecretProperties.cs b/SW.Bitween.Api/Services/AdapterSecretProperties.cs index e5f13e35..7de94d22 100644 --- a/SW.Bitween.Api/Services/AdapterSecretProperties.cs +++ b/SW.Bitween.Api/Services/AdapterSecretProperties.cs @@ -8,14 +8,14 @@ namespace SW.Bitween; /// -/// Keeps adapter secrets — an api key, a mail password — out of responses, and puts them back when +/// Keeps adapter secrets - an api key, a mail password - out of responses, and puts them back when /// an unchanged one is saved again. /// /// /// /// An adapter marks a startup value [Secure], which is what /// reports. replaces those values with on the way out, and -/// reads the sentinel on the way back in as "keep what is stored" — so a form +/// reads the sentinel on the way back in as "keep what is stored" - so a form /// that only changed the subject line does not overwrite the password with a row of dots. /// /// @@ -68,7 +68,7 @@ public async Task> Mask( /// secrets nobody can ask an adapter about. /// /// - /// A partner property or a global value is not a startup value of any one adapter — it is + /// A partner property or a global value is not a startup value of any one adapter - it is /// referenced as {{partner.KEY}} by however many adapters point at it, so there is no /// single adapter to describe. The owner names its own secrets instead, exactly as a data /// source does. Unlike DataSources.Secrets this does not also guess from the key's name: @@ -111,11 +111,56 @@ public static Dictionary Merge( return result; } + /// + /// Whether a sentinel may still mean "keep what is stored". Not once the adapter is a different + /// one, or a property saying where it connects has changed: a secret kept across that goes to + /// wherever the editor pointed it - their own server, say - without their ever having seen it. + /// Re-entering it then proves they know it. + /// + public static bool MayKeepStoredSecrets(string storedAdapterId, string newAdapterId, + IReadOnlyDictionary stored, IEnumerable> incoming) + { + if (!string.Equals(storedAdapterId ?? "", newAdapterId ?? "", StringComparison.OrdinalIgnoreCase)) + return false; + + var before = stored ?? new Dictionary(); + var after = (incoming ?? []).Where(kv => kv.Key != null) + .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Last().Value, StringComparer.OrdinalIgnoreCase); + + foreach (var key in before.Keys.Concat(after.Keys).Where(IsDestination).Distinct(StringComparer.OrdinalIgnoreCase)) + { + before.TryGetValue(key, out var was); + after.TryGetValue(key, out var now); + if (now == Sentinel) continue; + if (!string.Equals(was ?? "", now ?? "", StringComparison.Ordinal)) return false; + } + + return true; + } + + private static readonly string[] DestinationWords = + ["url", "uri", "host", "server", "endpoint", "address", "domain", "port", "connectionstring"]; + + /// A property that says where an adapter connects, judged by its name. + public static bool IsDestination(string key) => + key != null && DestinationWords.Any(w => key.Replace("_", "").Replace("-", "") + .Contains(w, StringComparison.OrdinalIgnoreCase)); + + /// + /// , + /// except that when is false a sentinel restores nothing - the + /// secret has to be entered again. + /// + public static Dictionary Merge(IReadOnlyDictionary stored, + IReadOnlyDictionary incoming, bool keepStored) => + Merge(keepStored ? stored : null, incoming); + /// /// applied to the dictionary the caller already holds. /// /// - /// is an immutable value object — every property is init — so a + /// is an immutable value object - every property is init - so a /// group's properties cannot be swapped for a masked copy. Editing the dictionary in place is /// the way to reach them without either loosening that contract or rebuilding each group /// property by property, which would silently drop whatever property is added to it next. diff --git a/SW.Bitween.Api/Services/AdapterStartupValues.cs b/SW.Bitween.Api/Services/AdapterStartupValues.cs index 59f31a40..67c87cca 100644 --- a/SW.Bitween.Api/Services/AdapterStartupValues.cs +++ b/SW.Bitween.Api/Services/AdapterStartupValues.cs @@ -7,7 +7,7 @@ namespace SW.Bitween; /// -/// What startup properties an adapter expects — its key names, which are optional, which are +/// What startup properties an adapter expects - its key names, which are optional, which are /// secret, and their defaults. /// /// @@ -26,18 +26,57 @@ namespace SW.Bitween; public class AdapterStartupValues( NativeAdapterDiscoveryService nativeAdapterDiscovery, ServerlessAdapterDescriber serverlessDescriber, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + AdapterCatalog catalog = null) { /// Drops what is remembered about a published adapter. public void Forget(string adapterId) => serverlessDescriber.Forget(adapterId); - /// Native (native prefix) or published. + /// + /// Native (native prefix) or published - or a published adapter pinned to a version, + /// {id}/{version}, whose properties may differ from the current one's. + /// /// Key name to description. Empty when the adapter reports nothing. + /// + /// For a screen asking about one adapter: one that isn't published, or whose package can't be + /// read, is not found (404) with the reason, rather than the 500 it was. + /// + public async Task> DescribeOrNotFound(string adapterId) + { + try + { + return await Describe(adapterId); + } + catch (InvalidOperationException ex) + { + throw new SW.PrimitiveTypes.SWNotFoundException($"Adapter '{adapterId}' can't be described: {ex.Message}"); + } + } + public async Task> Describe(string adapterId) { if (string.IsNullOrWhiteSpace(adapterId)) return new Dictionary(); + // A published package that lists its properties in its manifest is described from that - + // no process to start, and the same answer for a resident adapter as a classic one. A + // package without one, or one that lists none, is asked as it always was. + if (catalog != null && !NativeAdapterDiscoveryService.IsNative(adapterId)) + { + var manifest = await catalog.ManifestOf(adapterId); + if (manifest?.Properties is { Count: > 0 } declared) + return FromManifest(declared); + + // An adapter in another runtime than .NET cannot be asked: it speaks only gRPC, and + // asking means the stdio exchange of .NET adapters, which it would leave waiting for a + // reply until the describer timed out - holding one of its few slots all the while. + // Nor does it need asking. Its manifest was written from what it said when it was + // built, so a manifest that lists nothing means it expects nothing. + if (manifest != null && !string.IsNullOrWhiteSpace(manifest.Runtime) && + !string.Equals(manifest.Runtime, SW.Serverless.Contract.Catalog.AdapterManifest.DotnetRuntime, StringComparison.OrdinalIgnoreCase)) + return FromManifest(manifest.Properties ?? []); + } + // Reflection over an in-process type, so there is nothing here worth caching, and nothing // worth queueing behind the published adapters either. if (adapterId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) @@ -50,20 +89,37 @@ public async Task> Describe(string adapterId) // // Every caller of this then failed in its own way and none named a cause: saving a // subscription that used one was refused outright, and the two that mask secrets failed - // closed and returned every property as "__private__" — so a screen showed a masked value + // closed and returned every property as "__private__" - so a screen showed a masked value // where the chosen statement should be, and its dropdown could not match it. // // Nothing is the right answer rather than a shrug. A resident adapter's settings live on // its DATA SOURCE, which describes and masks its own; what a subscription holds for one - // is which statement to run and what to do with it — routing, not secrets. + // is which statement to run and what to do with it - routing, not secrets. if (await ResidentAdapters.IsResidentAsync(serviceProvider, adapterId)) return new Dictionary(); return await serverlessDescriber.Describe(adapterId); } + /// A manifest's properties in the shape every caller of already reads. + public static IDictionary FromManifest( + IEnumerable properties) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var property in properties) + result[property.Name] = new StartupValue + { + Optional = !property.Required, + Default = property.Default, + Type = property.Type ?? SW.Serverless.Contract.Catalog.AdapterProperty.TextType, + Private = property.Secret, + Description = property.Description ?? property.DisplayName + }; + return result; + } + /// - /// The same description with the default withheld from every secret property — the shape to + /// The same description with the default withheld from every secret property - the shape to /// hand to a client. /// /// diff --git a/SW.Bitween.Api/Services/Adapters/AdapterCatalog.cs b/SW.Bitween.Api/Services/Adapters/AdapterCatalog.cs new file mode 100644 index 00000000..189e1c61 --- /dev/null +++ b/SW.Bitween.Api/Services/Adapters/AdapterCatalog.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; + +namespace SW.Bitween.Services.Adapters; + +/// +/// The published adapters' catalog - current version, every version with its manifest, the icon - +/// and the manifests of the native ones, behind one lookup. +/// +/// +/// A published adapter has a catalog entry only once it has been published by an installer that +/// writes one. Every answer here is therefore optional, and every caller falls back to what it did +/// before catalogs existed: list the package files, describe the adapter by asking it. +/// +public class AdapterCatalog( + ICloudFilesService cloudFiles, + ServerlessOptions serverlessOptions, + NativeAdapterManifests nativeManifests, + IMemoryCache cache) +{ + /// + /// What adapters call Bitween in their manifest's compatibility.applications - the minimum + /// Bitween they need - and what manifests before that field called minBitweenVersion. + /// + public const string Application = "bitween"; + + // Long enough that a screen listing every kind reads each entry once; short enough that a + // publish shows up without anyone thinking to clear anything. + static readonly TimeSpan CacheFor = TimeSpan.FromSeconds(60); + + AdapterCatalogStore Store => new(cloudFiles, serverlessOptions.AdapterRemotePath); + + static string CacheKey(string adapterId) => $"bitween.adapter-catalog.{adapterId.ToLowerInvariant()}"; + + /// The adapter's catalog entry, or null when it has none - native, or published before catalogs. + public async Task GetAsync(string adapterId) + { + if (string.IsNullOrWhiteSpace(adapterId) || NativeAdapterDiscoveryService.IsNative(adapterId)) return null; + + if (cache.TryGetValue(CacheKey(adapterId), out AdapterCatalogEntry cached)) return cached; + + AdapterCatalogEntry entry; + try + { + entry = await Store.GetAsync(adapterId); + } + catch + { + // A catalog that cannot be read is treated as absent: everything that uses it has a + // fallback, and a storage hiccup must not empty the adapter lists. + return null; + } + + cache.Set(CacheKey(adapterId), entry, CacheFor); + return entry; + } + + public void Forget(string adapterId) + { + if (!string.IsNullOrWhiteSpace(adapterId)) cache.Remove(CacheKey(adapterId)); + } + + /// + /// The manifest of what runs: a native adapter's own, a pinned + /// version's, or the current package's. Null when there is none to read. + /// + public async Task ManifestOf(string adapterRef) + { + if (string.IsNullOrWhiteSpace(adapterRef)) return null; + if (NativeAdapterDiscoveryService.IsNative(adapterRef)) return nativeManifests.Get(adapterRef); + + var (adapterId, version) = AdapterCatalogPaths.Split(adapterRef); + var entry = await GetAsync(adapterId); + if (entry == null) return null; + return version == null ? entry.Manifest : entry.Find(version)?.Manifest; + } + + /// + /// Whether of the adapter can be pinned: published and not withdrawn. + /// Without a catalog entry, published means its package exists. + /// + public async Task HasVersionAsync(string adapterId, string version) + { + if (!AdapterCatalogPaths.IsVersion(version)) return false; + + var entry = await GetAsync(adapterId); + if (entry != null) + return entry.Find(version) is { Withdrawn: false }; + + // Where versions are published now, or where an installer from before the versions prefix put them. + foreach (var key in new[] + { + AdapterCatalogPaths.Version(serverlessOptions.AdapterRemotePath, adapterId, version), + AdapterCatalogPaths.LegacyVersion(serverlessOptions.AdapterRemotePath, adapterId, version) + }) + if ((await cloudFiles.ListAsync(key)).Any(f => string.Equals(f.Key, key, StringComparison.Ordinal) && f.Size > 0)) + return true; + return false; + } +} diff --git a/SW.Bitween.Api/Services/Adapters/AdapterChanges.cs b/SW.Bitween.Api/Services/Adapters/AdapterChanges.cs new file mode 100644 index 00000000..ab118711 --- /dev/null +++ b/SW.Bitween.Api/Services/Adapters/AdapterChanges.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Services.Adapters; + +/// Told to every node when an adapter's published versions change. +public class AdapterChangedMessage +{ + public string AdapterId { get; set; } +} + +/// +/// Forgets what this node and every other has cached about an adapter: its catalog entry and its +/// described properties. Each node caches both on its own, so forgetting only on the node that +/// published left the others showing the old version and its old properties until their caches +/// expired. The broadcast is best effort, like the configuration cache's: the publish has happened +/// whether or not the bus could be told, and the caches still expire on their own. +/// +public class AdapterChanges(AdapterCatalog catalog, AdapterStartupValues startupValues, + IServiceScopeFactory scopes, ILogger logger) +{ + public async Task ForgetEverywhereAsync(string adapterId) + { + Forget(adapterId); + try + { + using var scope = scopes.CreateScope(); + var broadcast = scope.ServiceProvider.GetService(); + if (broadcast != null) await broadcast.Broadcast(new AdapterChangedMessage { AdapterId = adapterId }); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Couldn't tell the other nodes that {AdapterId} changed; they show it as cached until it expires.", adapterId); + } + } + + public void Forget(string adapterId) + { + catalog.Forget(adapterId); + startupValues.Forget(adapterId); + } +} + +public class AdapterChangedListener(AdapterChanges changes) : IListen +{ + public Task Process(AdapterChangedMessage message) + { + if (!string.IsNullOrWhiteSpace(message?.AdapterId)) changes.Forget(message.AdapterId); + return Task.CompletedTask; + } +} diff --git a/SW.Bitween.Api/Services/Adapters/AdapterFailureReader.cs b/SW.Bitween.Api/Services/Adapters/AdapterFailureReader.cs index 375934e6..17fd70f0 100644 --- a/SW.Bitween.Api/Services/Adapters/AdapterFailureReader.cs +++ b/SW.Bitween.Api/Services/Adapters/AdapterFailureReader.cs @@ -11,14 +11,14 @@ namespace SW.Bitween.Services.Adapters; /// /// Turns what a dying adapter printed into a sentence an operator can act on. /// -/// An adapter that fails during startup — a broker refusing the credentials, TLS attempted against -/// a plaintext port — writes the reason to its own stderr and exits. The host keeps that output, +/// An adapter that fails during startup - a broker refusing the credentials, TLS attempted against +/// a plaintext port - writes the reason to its own stderr and exits. The host keeps that output, /// but nothing carried it anywhere a person looks: the connection test said "Adapter stream /// closed", the data source row's LastException stayed null because that field only ever held what /// a LIVE adapter reported about itself, and the actual exception existed solely in the API log. /// -/// So the two questions an operator asks — "why did the test fail" and "why is this connection -/// down" — could only be answered by tailing a log on the node. This is what makes them +/// So the two questions an operator asks - "why did the test fail" and "why is this connection +/// down" - could only be answered by tailing a log on the node. This is what makes them /// answerable from the screen that asked. /// public static class AdapterFailureReader @@ -30,7 +30,7 @@ public static class AdapterFailureReader /// The most useful thing in the adapter's last output, or null when it said nothing. /// /// Prefers the last error it logged, because an adapter that dies logs the reason last. Falls - /// back to the final line of anything at all — an adapter can also die without using the SDK's + /// back to the final line of anything at all - an adapter can also die without using the SDK's /// logger, and half an answer beats none. /// public static string Summarise(IEnumerable diagnostics) @@ -68,8 +68,8 @@ public static string Explain(string message, IEnumerable diagnostics) /// thread. Waiting for the process to exit is what makes the difference between "Adapter /// stream closed" and the exception that closed it. /// - /// The wait is short and bounded, and an adapter that is still alive — a timeout rather than a - /// crash — is not waited on at all. + /// The wait is short and bounded, and an adapter that is still alive - a timeout rather than a + /// crash - is not waited on at all. /// public static async Task> SettledOutputAsync(ResidentAdapterInstance instance, int graceMilliseconds = 2000) @@ -150,7 +150,7 @@ private static string Clean(string line) .Trim(); // The SDK writes "message#!#exception#!#", so what is left is the message, then the - // exception's own first line — which is the type and text, and is the sentence to lead + // exception's own first line - which is the type and text, and is the sentence to lead // with. Everything past it is a stack trace nobody reads on a data source screen. var parts = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length == 0) return null; @@ -166,7 +166,7 @@ private static string Clean(string line) .Select(l => l[4..].Trim()) .LastOrDefault(l => !string.IsNullOrWhiteSpace(l) && !lead.Contains(l, StringComparison.Ordinal)); - var summary = cause == null ? lead : $"{lead} — caused by {cause}"; + var summary = cause == null ? lead : $"{lead} - caused by {cause}"; if (string.IsNullOrWhiteSpace(summary)) return null; diff --git a/SW.Bitween.Api/Services/Adapters/AdapterInvoker.cs b/SW.Bitween.Api/Services/Adapters/AdapterInvoker.cs index 42a03f3a..5d24de09 100644 --- a/SW.Bitween.Api/Services/Adapters/AdapterInvoker.cs +++ b/SW.Bitween.Api/Services/Adapters/AdapterInvoker.cs @@ -1,7 +1,12 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; +using SW.Bitween.Services.Metrics; using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; namespace SW.Bitween.Services.Adapters; @@ -10,9 +15,14 @@ namespace SW.Bitween.Services.Adapters; /// /// The pipeline asks for a mapper, handler, validator or receiver by id and role; which of the /// three runtimes ends up running it is decided here and nowhere else. That is what keeps the -/// choice from being copied into every call site — and what lets a test replace the whole thing. +/// choice from being copied into every call site - and what lets a test replace the whole thing. +/// +/// Every adapter call passes through here, so this is also where each one is counted for the +/// adapter metrics: how long it took, and whether it failed and how - a timeout, a stop for going +/// over a limit, a process that died, or an error of the adapter's own. /// -public class AdapterInvoker(IEnumerable runtimes) : IAdapterInvoker +public class AdapterInvoker(IEnumerable runtimes, MetricsRecorder metrics = null, AdapterCatalog catalog = null) + : IAdapterInvoker { // Order matters: the classic runtime claims everything, so it has to be asked last. private readonly IReadOnlyList _runtimes = runtimes.ToList(); @@ -22,7 +32,22 @@ public async Task BeginAsync(string adapterId, AdapterRole role { foreach (var runtime in _runtimes) if (await runtime.CanRunAsync(adapterId)) - return await runtime.BeginAsync(adapterId, role, properties, correlationId); + { + if (metrics == null) return await runtime.BeginAsync(adapterId, role, properties, correlationId); + + var meter = new Meter(metrics, adapterId, await VersionOf(adapterId), role, RuntimeKind(runtime)); + var clock = Stopwatch.StartNew(); + try + { + return new MeteredSession(await runtime.BeginAsync(adapterId, role, properties, correlationId), meter); + } + catch (Exception ex) + { + // A session that couldn't even start is a failed call: a process that wouldn't spawn. + meter.Record(clock.ElapsedMilliseconds, ex); + throw; + } + } throw new BitweenException($"No runtime here knows how to run adapter '{adapterId}'."); } @@ -33,4 +58,88 @@ public async Task InvokeAsync(string adapterId, AdapterRole ro await using var session = await BeginAsync(adapterId, role, properties, correlationId); return await session.InvokeAsync(method, argument); } + + /// + /// The version that runs: the pinned one, or for an unpinned reference the catalog's current + /// version now (read from the catalog's one-minute cache, which the runtimes have just read). + /// + async Task VersionOf(string adapterRef) + { + if (string.IsNullOrWhiteSpace(adapterRef) || NativeAdapterDiscoveryService.IsNative(adapterRef)) return ""; + var (id, version) = AdapterCatalogPaths.Split(adapterRef); + if (version != null) return version; + try + { + return catalog == null ? "" : (await catalog.GetAsync(id))?.Current ?? ""; + } + catch + { + return ""; + } + } + + static string RuntimeKind(IAdapterRuntime runtime) => runtime switch + { + NativeAdapterRuntime => "native", + ResidentAdapterRuntime => "resident", + ClassicAdapterRuntime => "process", + _ => runtime.GetType().Name.ToLowerInvariant(), + }; + + /// How a failed call failed, from what the runtimes throw. + public static AdapterFailure Classify(Exception ex) => ex switch + { + null => AdapterFailure.None, + TimeoutException => AdapterFailure.Timeout, + SW.Serverless.Resident.AdapterStoppedException { LimitExceeded: true } => AdapterFailure.LimitStop, + SW.Serverless.Resident.AdapterStoppedException => AdapterFailure.Crash, + IOException io when io.Message.Contains("stream closed", StringComparison.OrdinalIgnoreCase) => AdapterFailure.Crash, + _ when ex.Message.Contains("Process not started or terminated", StringComparison.OrdinalIgnoreCase) => AdapterFailure.Crash, + _ => AdapterFailure.Error, + }; + + sealed class Meter(MetricsRecorder metrics, string adapterRef, string version, AdapterRole role, string runtime) + { + readonly string _adapterId = AdapterCatalogPaths.Split(adapterRef).Item1; + + public void Record(long ms, Exception ex) => + metrics.AdapterCall(_adapterId, version, role.ToString().ToLowerInvariant(), runtime, ms, Classify(ex), ex?.Message); + } + + /// A session whose every call is timed and counted. + sealed class MeteredSession(IAdapterSession inner, Meter meter) : IAdapterSession + { + public async Task InvokeAsync(string method, object argument = null) + { + var clock = Stopwatch.StartNew(); + try + { + var result = await inner.InvokeAsync(method, argument); + meter.Record(clock.ElapsedMilliseconds, null); + return result; + } + catch (Exception ex) + { + meter.Record(clock.ElapsedMilliseconds, ex); + throw; + } + } + + public async Task InvokeAsync(string method, object argument = null) + { + var clock = Stopwatch.StartNew(); + try + { + await inner.InvokeAsync(method, argument); + meter.Record(clock.ElapsedMilliseconds, null); + } + catch (Exception ex) + { + meter.Record(clock.ElapsedMilliseconds, ex); + throw; + } + } + + public ValueTask DisposeAsync() => inner.DisposeAsync(); + } } diff --git a/SW.Bitween.Api/Services/Adapters/AdapterSourceReader.cs b/SW.Bitween.Api/Services/Adapters/AdapterSourceReader.cs new file mode 100644 index 00000000..42720129 --- /dev/null +++ b/SW.Bitween.Api/Services/Adapters/AdapterSourceReader.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; + +namespace SW.Bitween.Services.Adapters; + +/// The source a published version carries, as listed in its manifest. +public class AdapterSourceListing +{ + public string AdapterId { get; set; } + public string Version { get; set; } + public string Language { get; set; } + public string Runtime { get; set; } + public string BuildCommand { get; set; } + public List Lockfiles { get; set; } = []; + + /// Path relative to the source folder, and its SHA-256: enough to tell which files two versions differ in. + public List Files { get; set; } = []; +} + +public class AdapterSourceFileInfo +{ + public string Path { get; set; } + public string Sha256 { get; set; } +} + +public class AdapterSourceFile +{ + public string Path { get; set; } + public string Sha256 { get; set; } + public long Size { get; set; } + + /// True when the file is not text, or too large to show; is then null. + public bool Binary { get; set; } + + public string Content { get; set; } +} + +/// +/// Reads the source a published adapter version carries - serverless build puts it in the +/// package under the manifest's source folder, whatever the language - so it can be shown and +/// compared in Bitween. Packages published before that, or with --no-source, carry none, +/// and say so by having no source in their manifest. +/// +/// +/// The list comes from the catalog's manifest and costs no download. A file comes from the +/// package itself, and is checked against the hash its manifest recorded, so what is shown is +/// what was published. A package's source is read once and kept a few minutes: comparing two +/// versions reads several files of each. +/// +public class AdapterSourceReader( + AdapterCatalog catalog, + ICloudFilesService cloudFiles, + ServerlessOptions serverlessOptions, + IMemoryCache cache) +{ + /// Larger than this is shown as binary: a source file this size is almost always generated. + public const int MaxShownBytes = 1024 * 1024; + + /// A package whose source is larger than this is not read into memory at all. + public const long MaxSourceBytes = 50L * 1024 * 1024; + + static readonly TimeSpan CacheFor = TimeSpan.FromMinutes(5); + + /// Null when the adapter or version is unknown; an empty listing when it carries no source. + public async Task ListAsync(string adapterId, string version) + { + var manifest = await ManifestOf(adapterId, version); + if (manifest == null) return null; + + return new AdapterSourceListing + { + AdapterId = adapterId, + Version = version, + Language = manifest.Language, + Runtime = manifest.Runtime, + BuildCommand = manifest.Source?.BuildCommand, + Lockfiles = manifest.Source?.Lockfiles ?? [], + Files = (manifest.Source?.Files ?? []) + .OrderBy(f => f.Key, StringComparer.Ordinal) + .Select(f => new AdapterSourceFileInfo { Path = f.Key, Sha256 = f.Value }) + .ToList() + }; + } + + /// Null when the version, or the file in it, is unknown. + public async Task ReadAsync(string adapterId, string version, string path) + { + var manifest = await ManifestOf(adapterId, version); + // Only what the manifest lists can be asked for, so a path can never reach outside the + // source folder, nor into the package's binaries. + if (manifest?.Source?.Files == null || path == null || !manifest.Source.Files.TryGetValue(path, out var expected)) + return null; + + var files = await SourceOf(adapterId, version, manifest.Source); + if (!files.TryGetValue(path, out var bytes)) + throw new SWException($"Version {version} of '{adapterId}' lists {path} in its manifest, but its package does not carry it."); + + var actual = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase)) + throw new SWException($"{path} in version {version} of '{adapterId}' does not match the hash its manifest recorded; it is not shown."); + + var text = bytes.Length <= MaxShownBytes ? AsText(bytes) : null; + return new AdapterSourceFile + { + Path = path, + Sha256 = actual, + Size = bytes.Length, + Binary = text == null, + Content = text + }; + } + + async Task ManifestOf(string adapterId, string version) + { + if (string.IsNullOrWhiteSpace(adapterId) || !AdapterCatalogPaths.IsVersion(version)) return null; + var entry = await catalog.GetAsync(adapterId); + return entry?.Find(version)?.Manifest; + } + + async Task> SourceOf(string adapterId, string version, AdapterSource source) + { + var key = $"bitween.adapter-source.{adapterId.ToLowerInvariant()}.{version.ToLowerInvariant()}"; + if (cache.TryGetValue(key, out Dictionary cached)) return cached; + + var files = await Download(adapterId, version, source); + cache.Set(key, files, CacheFor); + return files; + } + + async Task> Download(string adapterId, string version, AdapterSource source) + { + var root = serverlessOptions.AdapterRemotePath; + var packageKey = AdapterCatalogPaths.Version(root, adapterId, version); + if (!await Exists(packageKey)) + { + var legacy = AdapterCatalogPaths.LegacyVersion(root, adapterId, version); + if (!await Exists(legacy)) + throw new SWException($"The package of version {version} of '{adapterId}' is not in storage."); + packageKey = legacy; + } + + // A zip is read from its end, so it goes to a temp file first rather than being held whole. + var temp = Path.GetTempFileName(); + try + { + await using (var remote = await cloudFiles.OpenReadAsync(packageKey)) + await using (var local = File.Create(temp)) + await remote.CopyToAsync(local); + + var prefix = (string.IsNullOrWhiteSpace(source.Path) ? AdapterSource.DefaultPath : source.Path).Trim('/') + "/"; + var files = new Dictionary(StringComparer.Ordinal); + long total = 0; + using var zip = ZipFile.OpenRead(temp); + foreach (var entry in zip.Entries) + { + // Zips written on Windows separate with '\'. + var name = entry.FullName.Replace('\\', '/'); + if (!name.StartsWith(prefix, StringComparison.Ordinal) || name.EndsWith('/')) continue; + var relative = name[prefix.Length..]; + if (!source.Files.ContainsKey(relative)) continue; + + total += entry.Length; + if (total > MaxSourceBytes) + throw new SWException($"The source of version {version} of '{adapterId}' is larger than Bitween reads."); + + await using var stream = entry.Open(); + using var buffer = new MemoryStream((int)entry.Length); + await stream.CopyToAsync(buffer); + files[relative] = buffer.ToArray(); + } + + return files; + } + finally + { + try { File.Delete(temp); } catch { } + } + } + + async Task Exists(string key) => + (await cloudFiles.ListAsync(key)).Any(f => string.Equals(f.Key, key, StringComparison.Ordinal) && f.Size > 0); + + /// The file as UTF-8 text, or null when it isn't text. + internal static string AsText(byte[] bytes) + { + // A NUL byte is the reliable tell of a binary file; text in any encoding a source file + // would use has none. + if (Array.IndexOf(bytes, (byte)0) >= 0) return null; + try + { + var text = new UTF8Encoding(false, true).GetString(bytes); + return text.Length > 0 && text[0] == '' ? text[1..] : text; + } + catch (DecoderFallbackException) + { + return null; + } + } +} diff --git a/SW.Bitween.Api/Services/Adapters/AdapterWorkshop.cs b/SW.Bitween.Api/Services/Adapters/AdapterWorkshop.cs new file mode 100644 index 00000000..2ebabd34 --- /dev/null +++ b/SW.Bitween.Api/Services/Adapters/AdapterWorkshop.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; +using SW.Serverless.Tooling; +using SW.Serverless.Tooling.Building; +using SW.Serverless.Tooling.Conformance; +using SW.Serverless.Tooling.Scaffolding; +using SW.Bitween.Adapters.Tooling; + +namespace SW.Bitween.Services.Adapters; + +public class WorkshopBuild +{ + public bool Succeeded { get; set; } + public List Problems { get; set; } = []; + public List Warnings { get; set; } = []; + + /// What the adapter says it is, once built: its settings and commands, for the try panel's form. + public AdapterManifest Manifest { get; set; } + + public List Checks { get; set; } = []; + public bool Conforms => Succeeded && Checks.All(c => c.Outcome != nameof(CheckOutcome.Failed)); +} + +public record WorkshopCheck(string Name, string Outcome, string Detail); + +public class WorkshopRun +{ + public bool Succeeded { get; set; } + public string Output { get; set; } + public string Error { get; set; } + public List Problems { get; set; } = []; +} + +/// +/// The adapter editor's server side: drafts started from a template or a published version, then +/// built, checked, tried and published with SW.Serverless.Tooling - the same code bitween adapter build, +/// test, run and publish are - so a version published here is the one the CLI would have published. +/// +/// +/// +/// Python and JavaScript/TypeScript only, and their own code only: the image has their runtimes but +/// neither pip nor npm, and a dependency would mean fetching packages from the internet in the +/// middle of a save. A draft that names one is refused with the CLI as the way to add it. +/// +/// +/// Building and trying run the draft's code on this server. That is what the permissions are for: +/// adapter-source.edit to do it at all, adapter-source.operate to publish. At most +/// two run at a time, so the editor can't take the server's processing down with it. +/// +/// +public class AdapterWorkshop( + AdapterCatalog catalog, + AdapterSourceReader sourceReader, + AdapterChanges changes, + ICloudFilesService cloudFiles, + ServerlessOptions serverlessOptions, + BitweenOptions bitweenOptions) +{ + static readonly SemaphoreSlim Running = new(2, 2); + const int CallTimeoutSeconds = 30; + + /// + /// What a draft may use while it is checked or tried here, besides the two at a time and the + /// timeout per call: memory and sustained CPU, from Bitween:AdapterEditorMemoryMb and + /// AdapterEditorCpuCores. A run over either is stopped, and the editor shows which limit it hit. + /// + LocalAdapterLimits Limits => new() + { + MemoryLimitBytes = Math.Max(0, bitweenOptions.AdapterEditorMemoryMb) * 1024L * 1024L, + CpuPercentLimit = Math.Max(0, bitweenOptions.AdapterEditorCpuCores) * 100.0 / Environment.ProcessorCount, + }; + + // ------------------------------------------------------------------ starting a draft + + /// A new adapter, from the template serverless init writes. + public async Task NewAsync(string name, string language, string kind, string adapterId) + { + if (!AdapterDraft.Languages.Contains(language)) + throw new SWValidationException("Language", $"The editor writes {string.Join(", ", AdapterDraft.Languages)} adapters."); + + var id = string.IsNullOrWhiteSpace(adapterId) ? Scaffolder.IdFrom(name ?? "") : adapterId.Trim().ToLowerInvariant(); + if (await catalog.GetAsync(id) != null) + throw new SWValidationException("AdapterId", $"'{id}' is already published; edit one of its versions instead."); + + var parent = Path.Combine(Path.GetTempPath(), "bitween-workshop", Guid.NewGuid().ToString("N")); + try + { + var made = BitweenAdapters.Scaffold(new ScaffoldRequest { Name = name, Id = id, Language = language, Kind = kind, ParentDirectory = parent }); + if (!made.Succeeded) throw new SWValidationException("Name", string.Join("; ", made.Problems)); + var files = made.Files.ToDictionary(f => f.Replace('\\', '/'), f => File.ReadAllText(Path.Combine(made.ProjectDirectory, f))); + return new AdapterDraft(id, language, kind, null, files); + } + finally + { + try { Directory.Delete(parent, true); } catch { } + } + } + + /// A draft of a published Python or Node version, from the source its package carries. + public async Task FromVersionAsync(string adapterId, string version) + { + var listing = await sourceReader.ListAsync(adapterId, version) + ?? throw new SWNotFoundException($"{adapterId} {version}"); + var language = listing.Runtime switch + { + AdapterManifest.PythonRuntime => "python", + AdapterManifest.NodeRuntime => listing.Language == "typescript" ? "typescript" : "node", + _ => throw new SWValidationException("AdapterId", + $"Version {version} of '{adapterId}' runs on {listing.Runtime ?? "dotnet"}; the editor writes Python and JavaScript adapters. Build it with the CLI."), + }; + if (listing.Files.Count == 0) + throw new SWValidationException("Version", $"Version {version} of '{adapterId}' was published without its source."); + + var files = new Dictionary(); + foreach (var file in listing.Files) + { + var read = await sourceReader.ReadAsync(adapterId, version, file.Path); + if (read is { Binary: false }) files[file.Path] = read.Content; + } + + var manifest = await catalog.ManifestOf(AdapterCatalogPaths.Ref(adapterId, version)); + return new AdapterDraft(adapterId, language, manifest?.Kinds.FirstOrDefault(), version, files); + } + + // ------------------------------------------------------------------ building, checking, trying + + /// Builds the draft and, unless only the build is wanted, checks it against its contracts. + public async Task BuildAsync(AdapterDraft draft, IDictionary settings, bool check) + { + await Running.WaitAsync(); + try + { + await using var built = await BuiltAsync(draft); + var result = built.Result; + if (!result.Succeeded || !check) return result; + + var report = await new ConformanceRunner().RunAsync(new ConformanceOptions + { + PackageDirectory = built.PackageDirectory, + Settings = settings ?? new Dictionary(), + CommandTimeoutSeconds = CallTimeoutSeconds, + // A receiver's DeleteFile changes the source it reads; never from here. + AllowDelete = false, + Contracts = { BitweenAdapters.Contract }, + Limits = Limits, + }); + result.Checks = report.Checks.Select(c => new WorkshopCheck(c.Name, c.Outcome.ToString(), c.Detail)).ToList(); + // The checks pass or fail on what the adapter does with the settings given. One it marks + // required but that was left empty was untested, while the result read as a clean pass. + foreach (var missing in (result.Manifest?.Properties ?? []).Where(p => p.Required && + (settings == null || !settings.TryGetValue(p.Name, out var v) || string.IsNullOrWhiteSpace(v)))) + result.Warnings.Add($"{missing.Name} is required but was empty, so the checks ran without it. " + + "A subscription has to give it a value."); + return result; + } + finally + { + Running.Release(); + } + } + + /// Builds the draft and calls one command, as a host would, with the settings given. + public async Task TryAsync(AdapterDraft draft, IDictionary settings, string command, string input) + { + if (string.IsNullOrWhiteSpace(command)) throw new SWValidationException("Command", "Say which command to call."); + await Running.WaitAsync(); + try + { + await using var built = await BuiltAsync(draft); + if (!built.Result.Succeeded) return new WorkshopRun { Problems = built.Result.Problems }; + try + { + await using var host = await LocalAdapterHost.StartAsync(built.PackageDirectory, settings ?? new Dictionary(), + commandTimeoutSeconds: CallTimeoutSeconds, limits: Limits); + return new WorkshopRun { Succeeded = true, Output = await host.CallAsync(command, ReadInput(input)) }; + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + return new WorkshopRun { Error = ex.GetBaseException().Message }; + } + } + finally + { + Running.Release(); + } + } + + // ------------------------------------------------------------------ publishing + + /// + /// Publishes the draft as a new version, not made current: subscriptions keep running what they + /// run until someone promotes it or pins it. It must build and pass its contract checks first. + /// + public async Task<(string Version, WorkshopBuild Build)> PublishAsync(AdapterDraft draft, string version, string releaseNotes, + IDictionary settings, string publishedBy) + { + await Running.WaitAsync(); + try + { + await using var built = await BuiltAsync(draft); + var result = built.Result; + if (!result.Succeeded) return (null, result); + + var report = await new ConformanceRunner().RunAsync(new ConformanceOptions + { + PackageDirectory = built.PackageDirectory, + Settings = settings ?? new Dictionary(), + CommandTimeoutSeconds = CallTimeoutSeconds, + Contracts = { BitweenAdapters.Contract }, + Limits = Limits, + }); + result.Checks = report.Checks.Select(c => new WorkshopCheck(c.Name, c.Outcome.ToString(), c.Detail)).ToList(); + if (!report.Passed) return (null, result); + + var published = await PackagePublisher.PublishPackageAsync(cloudFiles, new PublishPackageRequest + { + PackagePath = built.ZipPath, + Version = string.IsNullOrWhiteSpace(version) ? "patch" : version.Trim(), + Promote = false, + ReleaseNotes = releaseNotes, + PublishedBy = publishedBy, + RemotePath = serverlessOptions.AdapterRemotePath, + }, _ => { }); + + await changes.ForgetEverywhereAsync(draft.AdapterId); + return (published.Version, result); + } + finally + { + Running.Release(); + } + } + + /// Makes a published version the one that runs where nothing is pinned. + public async Task PromoteAsync(string adapterId, string version) + { + var work = Path.Combine(Path.GetTempPath(), "bitween-workshop", Guid.NewGuid().ToString("N")); + try + { + await new AdapterRepository(cloudFiles, _ => { }, serverlessOptions.AdapterRemotePath).PromoteAsync(adapterId, version, work); + } + finally + { + try { Directory.Delete(work, true); } catch { } + await changes.ForgetEverywhereAsync(adapterId); + } + } + + // ------------------------------------------------------------------ packages built elsewhere + + /// + /// Publishes a package the bitween CLI built - any language, .NET included - as it would publish + /// straight to storage, only through Bitween: its manifest is checked, its version settled, and it + /// is made current only when asked. + /// + public async Task UploadAsync(Stream zip, string version, bool current, string releaseNotes, string publishedBy) + { + var work = Path.Combine(Path.GetTempPath(), "bitween-workshop", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(work); + try + { + var path = Path.Combine(work, "package.zip"); + await using (var file = File.Create(path)) + await zip.CopyToAsync(file); + + PublishResult published; + try + { + published = await PackagePublisher.PublishPackageAsync(cloudFiles, new PublishPackageRequest + { + PackagePath = path, + Version = version, + Promote = current, + ReleaseNotes = releaseNotes, + PublishedBy = publishedBy, + RemotePath = serverlessOptions.AdapterRemotePath, + }, _ => { }); + } + catch (InvalidDataException) + { + throw new SWValidationException("Package", "That isn't a zip: send the package bitween adapter build made."); + } + + await changes.ForgetEverywhereAsync(published.Manifest.Id); + return published; + } + finally + { + try { Directory.Delete(work, true); } catch { } + } + } + + /// Every version of an adapter, with which is current and which are withdrawn. + public Task VersionsAsync(string adapterId) => + new AdapterRepository(cloudFiles, _ => { }, serverlessOptions.AdapterRemotePath).ListVersionsAsync(adapterId); + + /// Takes a version out of use: still listed, never offered for pinning, never made current. + public async Task WithdrawAsync(string adapterId, string version) + { + await new AdapterRepository(cloudFiles, _ => { }, serverlessOptions.AdapterRemotePath).WithdrawAsync(adapterId, version); + await changes.ForgetEverywhereAsync(adapterId); + } + + // ------------------------------------------------------------------ the build itself + + sealed class Built : IAsyncDisposable + { + public string Work { get; init; } + public WorkshopBuild Result { get; init; } + public string PackageDirectory { get; init; } + public string ZipPath { get; init; } + + public ValueTask DisposeAsync() + { + try { Directory.Delete(Work, true); } catch { } + return ValueTask.CompletedTask; + } + } + + async Task BuiltAsync(AdapterDraft draft) + { + var work = Path.Combine(Path.GetTempPath(), "bitween-workshop", Guid.NewGuid().ToString("N")); + var project = Path.Combine(work, "project"); + Directory.CreateDirectory(project); + + var files = draft.Files; + var result = new WorkshopBuild(); + result.Problems.AddRange(Dependencies(files, bitweenOptions.AdapterEditorDependencies)); + if (result.Problems.Count > 0) return new Built { Work = work, Result = result }; + + foreach (var (path, content) in files) + { + // A lock file can name any source, a local folder included; the build resolves from the + // registry instead, from package.json's plain versions. + if (path == "package-lock.json") + { + result.Warnings.Add("package-lock.json isn't used here: the editor installs the versions package.json names, from the registry"); + continue; + } + var target = Path.Combine(project, path); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + await File.WriteAllTextAsync(target, content); + } + + // The draft is for this adapter id whatever its adapter.json says; a typo there would + // otherwise publish someone else's adapter. + var manifestPath = Path.Combine(project, AdapterManifest.FileName); + if (!File.Exists(manifestPath)) + { + result.Problems.Add($"the draft has no {AdapterManifest.FileName}"); + return new Built { Work = work, Result = result }; + } + var authored = JsonNode.Parse(await File.ReadAllTextAsync(manifestPath), + documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true })?.AsObject(); + if (authored == null) + { + result.Problems.Add($"{AdapterManifest.FileName} isn't a JSON object"); + return new Built { Work = work, Result = result }; + } + if ((string)authored["id"] != draft.AdapterId) + { + result.Problems.Add($"{AdapterManifest.FileName} says \"id\": \"{(string)authored["id"]}\", but this draft is for {draft.AdapterId}"); + return new Built { Work = work, Result = result }; + } + + // With platforms that leave this server out, the build installs the requirements here a + // second time, to describe the adapter, and that install may build a package from source: + // running its setup code on the server. The CLI can do that on an author's machine; here, no. + if (files.ContainsKey("requirements.txt") && authored["platforms"] is JsonArray platforms && platforms.Count > 0 && + !platforms.Any(p => string.Equals((string)p, SW.Serverless.Runtimes.AdapterRuntimes.CurrentPlatform, StringComparison.OrdinalIgnoreCase))) + { + result.Problems.Add($"{AdapterManifest.FileName} lists platforms without {SW.Serverless.Runtimes.AdapterRuntimes.CurrentPlatform}, " + + "the one this server runs on; build it with bitween adapter build instead"); + return new Built { Work = work, Result = result }; + } + + var build = await PackageBuilder.BuildAsync(BitweenAdapters.BuildRequest(project, Path.Combine(work, "out"))); + result.Succeeded = build.Succeeded; + result.Problems.AddRange(build.Problems); + result.Warnings.AddRange(build.Warnings); + result.Manifest = build.Manifest; + return new Built { Work = work, Result = result, PackageDirectory = build.PackageDirectory, ZipPath = build.ZipPath }; + } + + static readonly string[] OwnPackages = ["sw-serverless", "sw_serverless", "simplyworks-bitween", "simplyworks_bitween", + "@simplyworks/sw-serverless", "@simplyworks/bitween"]; + + /// + /// What the editor won't build because of the packages a draft names. Without + /// Bitween:AdapterEditorDependencies, any beyond the SDKs. With it, any that isn't a plain name + /// and version from the registry: a URL, a path, a git source or a pip option could make the + /// server fetch from anywhere, or copy its own files into the package. + /// + static IEnumerable Dependencies(IDictionary files, bool allowed) + { + if (files.TryGetValue("requirements.txt", out var requirements)) + { + var lines = requirements.Split('\n').Select(l => l.Trim()) + .Where(l => l.Length > 0 && !l.StartsWith('#')) + .Select(l => l.Split(" #")[0].Trim()) + .ToList(); + var names = lines.Select(l => Regex.Match(l, @"^[A-Za-z0-9_.\-]+").Value) + .Where(n => !OwnPackages.Contains(n, StringComparer.OrdinalIgnoreCase)) + .ToList(); + if (names.Count > 0 && !allowed) + yield return $"requirements.txt names {string.Join(", ", names)}; this Bitween's editor builds adapters that need only the SDK. " + + "Build one with dependencies using bitween adapter build, which vendors them, or turn on Bitween:AdapterEditorDependencies"; + else if (allowed) + foreach (var line in lines.Where(l => !PlainRequirement.IsMatch(l))) + yield return $"requirements.txt: '{line}' isn't a package name and version; the editor fetches only those, from the package index"; + } + + if (files.TryGetValue("package.json", out var packageJson)) + { + JsonObject document = null; + try { document = JsonNode.Parse(packageJson)?.AsObject(); } catch (JsonException) { } + var dependencies = document?["dependencies"]?.AsObject() + .Where(d => !OwnPackages.Contains(d.Key, StringComparer.OrdinalIgnoreCase)).ToList() ?? []; + if (dependencies.Count > 0 && !allowed) + yield return $"package.json depends on {string.Join(", ", dependencies.Select(d => d.Key))}; this Bitween's editor builds adapters that need only the SDK. " + + "Build one with dependencies using bitween adapter build, which installs them, or turn on Bitween:AdapterEditorDependencies"; + else if (allowed) + foreach (var (name, spec) in dependencies) + if (!PlainPackageName.IsMatch(name) || spec is not JsonValue || !PlainVersionRange.IsMatch(spec.ToString())) + yield return $"package.json: \"{name}\": {spec?.ToJsonString()} isn't a package and version range; the editor installs only those, from the registry"; + + if (allowed && document?["optionalDependencies"] is JsonObject { Count: > 0 } || allowed && document?["bundleDependencies"] != null) + yield return "package.json: the editor installs dependencies only, not optional or bundled ones"; + } + } + + // name, optional [extras], optional version specifiers, optional ; marker. No URL, path or option. + static readonly Regex PlainRequirement = new( + @"^[A-Za-z0-9][A-Za-z0-9_.\-]*(\[[A-Za-z0-9_.,\- ]+\])?\s*((===?|!=|~=|<=?|>=?)\s*[A-Za-z0-9_.*+!\-]+\s*(,\s*(===?|!=|~=|<=?|>=?)\s*[A-Za-z0-9_.*+!\-]+\s*)*)?(;[^@:/\\]*)?$"); + + static readonly Regex PlainPackageName = new(@"^(@[a-z0-9][a-z0-9._\-]*/)?[a-z0-9][a-z0-9._\-]*$"); + + // Semver ranges and dist-tags: no "file:", "git+", "http:", "npm:" aliases or paths. + static readonly Regex PlainVersionRange = new(@"^[A-Za-z0-9 .*^~<>=|+\-]*$"); + + /// The try panel's input, as the CLI's run reads --input: JSON when it parses, the text otherwise. + static object ReadInput(string input) + { + if (string.IsNullOrEmpty(input)) return null; + try { return Newtonsoft.Json.Linq.JToken.Parse(input); } + catch (Newtonsoft.Json.JsonException) { return input; } + } +} diff --git a/SW.Bitween.Api/Services/Adapters/IAdapterRuntime.cs b/SW.Bitween.Api/Services/Adapters/IAdapterRuntime.cs index 6d6bf468..50fdde9d 100644 --- a/SW.Bitween.Api/Services/Adapters/IAdapterRuntime.cs +++ b/SW.Bitween.Api/Services/Adapters/IAdapterRuntime.cs @@ -20,14 +20,14 @@ public enum AdapterRole /// A run of calls against ONE adapter instance. /// /// A receiver is why this is a session rather than a single call: Initialize, ListFiles, a GetFile -/// and DeleteFile per item, then Finalize — all of which have to reach the same instance, or +/// and DeleteFile per item, then Finalize - all of which have to reach the same instance, or /// Initialize runs somewhere the listing never sees. /// public interface IAdapterSession : IAsyncDisposable { Task InvokeAsync(string method, object argument = null); - /// For a method that returns nothing — Initialize, DeleteFile, Finalize. + /// For a method that returns nothing - Initialize, DeleteFile, Finalize. Task InvokeAsync(string method, object argument = null); } @@ -42,7 +42,7 @@ public interface IAdapterRuntime { /// /// Whether this runtime is the one for that adapter. Async because deciding can mean reading - /// the adapter's published metadata — cached, but not free. + /// the adapter's published metadata - cached, but not free. /// Task CanRunAsync(string adapterId); @@ -53,7 +53,7 @@ Task BeginAsync(string adapterId, AdapterRole role, /// /// The one thing the pipeline calls to run an adapter. Everything that needs a mapper, handler, /// validator or receiver goes through here, so the choice of runtime is made once and in one -/// place — and so a test can replace the lot. +/// place - and so a test can replace the lot. /// public interface IAdapterInvoker { diff --git a/SW.Bitween.Api/Services/Adapters/NativeAdapterManifests.cs b/SW.Bitween.Api/Services/Adapters/NativeAdapterManifests.cs new file mode 100644 index 00000000..73ab7b39 --- /dev/null +++ b/SW.Bitween.Api/Services/Adapters/NativeAdapterManifests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using SW.Bitween.NativeAdapters; +using SW.Serverless.Contract.Catalog; + +namespace SW.Bitween.Services.Adapters; + +/// +/// The manifests the native, in-process adapters carry, as embedded adapter.json resources +/// beside their code. They describe and present an adapter the same way a published one's does - +/// display name, summary, publisher, icon, properties - so a catalog or marketplace treats both alike. +/// +/// +/// What a native adapter needs configured is still read from its startup-values type, which is the +/// code the adapter actually runs with; a test keeps the manifests' property lists from drifting +/// away from it. +/// +public class NativeAdapterManifests +{ + readonly Dictionary manifests; + + public NativeAdapterManifests(IEnumerable nativeAdapters) + { + manifests = new Dictionary(StringComparer.OrdinalIgnoreCase); + var assemblies = nativeAdapters.Select(a => a.GetType().Assembly) + .Append(typeof(INativeAdapter).Assembly) + .Distinct(); + + foreach (var assembly in assemblies) + foreach (var (id, manifest) in parsed.GetOrAdd(assembly, a => Read(a).ToList())) + manifests[id] = manifest; + } + + // Parsed once per assembly: this is resolved per request, and the resources never change. + static readonly System.Collections.Concurrent.ConcurrentDictionary> parsed = new(); + + /// The manifest for a native adapter, by its id (its type name); null when it has none. + public AdapterManifest Get(string adapterId) => + adapterId != null && manifests.TryGetValue(adapterId, out var manifest) ? manifest : null; + + public IReadOnlyCollection Ids => manifests.Keys; + + /// Every embedded resource named ….adapter.json, keyed by the manifest's id. + public static IEnumerable<(string Id, AdapterManifest Manifest)> Read(Assembly assembly) + { + foreach (var name in assembly.GetManifestResourceNames() + .Where(n => n.EndsWith("." + AdapterManifest.FileName, StringComparison.OrdinalIgnoreCase))) + { + using var stream = assembly.GetManifestResourceStream(name); + if (stream == null) continue; + using var reader = new StreamReader(stream); + var manifest = AdapterManifest.Parse(reader.ReadToEnd()); + if (!string.IsNullOrWhiteSpace(manifest.Id)) yield return (manifest.Id, manifest); + } + } +} diff --git a/SW.Bitween.Api/Services/Adapters/NativeAdapterRuntime.cs b/SW.Bitween.Api/Services/Adapters/NativeAdapterRuntime.cs index 56797996..6aca770c 100644 --- a/SW.Bitween.Api/Services/Adapters/NativeAdapterRuntime.cs +++ b/SW.Bitween.Api/Services/Adapters/NativeAdapterRuntime.cs @@ -8,11 +8,14 @@ namespace SW.Bitween.Services.Adapters; /// -/// Adapters that ship inside Bitween and run in-process. No packaging, no process, no protocol — +/// Adapters that ship inside Bitween and run in-process. No packaging, no process, no protocol - /// the only runtime where "invoke" is a method call. /// -public class NativeAdapterRuntime(NativeAdapterDiscoveryService discovery) : IAdapterRuntime +public class NativeAdapterRuntime(NativeAdapterDiscoveryService discovery, BitweenOptions options = null) : IAdapterRuntime { + /// Bitween:NativeAdapterTimeout, 900 seconds unless set; 0 for none. + TimeSpan? Limit => (options?.NativeAdapterTimeout ?? 900) is > 0 and var seconds ? TimeSpan.FromSeconds(seconds) : null; + public Task CanRunAsync(string adapterId) => Task.FromResult(adapterId != null && adapterId.StartsWith( NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)); @@ -33,19 +36,19 @@ public Task BeginAsync(string adapterId, AdapterRole role, _ => discovery.GetNativeHandler(adapterId, settings), }; - return Task.FromResult(new NativeAdapterSession(adapter)); + return Task.FromResult(new NativeAdapterSession(adapter, adapterId, Limit)); } /// /// Dispatches by method name onto the resolved adapter, so the pipeline can treat a native /// adapter exactly like a packaged one. /// - private sealed class NativeAdapterSession(object adapter) : IAdapterSession + private sealed class NativeAdapterSession(object adapter, string adapterId, TimeSpan? limit) : IAdapterSession { public async Task InvokeAsync(string method, object argument = null) => - (TResult)await Dispatch(method, argument); + (TResult)await Within(Dispatch(method, argument), limit, adapter, adapterId, method); - public Task InvokeAsync(string method, object argument = null) => Dispatch(method, argument); + public Task InvokeAsync(string method, object argument = null) => Within(Dispatch(method, argument), limit, adapter, adapterId, method); private async Task Dispatch(string method, object argument) { @@ -87,4 +90,27 @@ private async Task Dispatch(string method, object argument) // Nothing to release: a native adapter is an object, not a process or a lease. public ValueTask DisposeAsync() => ValueTask.CompletedTask; } + + /// + /// The call, or a once has passed. An in-process + /// call can't be stopped, only left: the adapter is disposed when it can be, which closes what it + /// holds open (a file server's connection), and the exchange fails as a timeout. The call left behind + /// may still finish, so a retry can deliver twice what a hung partner did in fact receive. + /// + internal static async Task Within(Task call, TimeSpan? limit, object adapter, string adapterId, string method) + { + if (limit is not { } wait) return await call; + try + { + return await call.WaitAsync(wait); + } + catch (TimeoutException) when (!call.IsCompleted) + { + _ = call.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted); + if (adapter is IDisposable disposable) + try { disposable.Dispose(); } catch { /* releasing only */ } + throw new TimeoutException( + $"{adapterId} did not finish {method} within {wait.TotalSeconds:0} seconds (Bitween:NativeAdapterTimeout), so it was left."); + } + } } diff --git a/SW.Bitween.Api/Services/Adapters/ResidentAdapterRuntime.cs b/SW.Bitween.Api/Services/Adapters/ResidentAdapterRuntime.cs index 15b0e6f2..9b63fd28 100644 --- a/SW.Bitween.Api/Services/Adapters/ResidentAdapterRuntime.cs +++ b/SW.Bitween.Api/Services/Adapters/ResidentAdapterRuntime.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.Services.Adapters; /// /// A packaged adapter that stays running, rented from the pool and invoked over its socket. /// -/// The two packaged lifecycles are published identically — same zip, same key, same metadata bag — +/// The two packaged lifecycles are published identically - same zip, same key, same metadata bag - /// and differ only in their entry point. Running a resident adapter down the classic path does not /// fail cleanly: the host spawns it and then waits out the command timeout for an answer that /// never comes, because the adapter dialled out and is waiting for a host that is not listening. @@ -54,7 +54,10 @@ public async Task CanRunAsync(string adapterId) public async Task BeginAsync(string adapterId, AdapterRole role, IDictionary properties, string correlationId) { - var adapters = serviceProvider.GetService() + // The host is on every node, for Python and Node adapters; running a resident adapter in + // the pipeline is still the opt-in it always was. + var enabled = serviceProvider.GetService()?.BusProvidersEnabled ?? true; + var adapters = (enabled ? serviceProvider.GetService() : null) ?? throw new BitweenException( $"Adapter '{adapterId}' is a resident adapter, but resident adapters are not " + "enabled on this node (Bitween:BusProvidersEnabled). It cannot run here."); @@ -70,7 +73,7 @@ public async Task BeginAsync(string adapterId, AdapterRole role spec.StartupValues[kv.Key] = kv.Value; } - // A subscription bound to a data source runs against THAT connection — the instance the + // A subscription bound to a data source runs against THAT connection - the instance the // supervisor already keeps up for it, holding the pool the data source exists to provide. // Renting instead would start a second process with a second pool, configured from // subscription properties that do not hold the credentials at all. @@ -82,16 +85,16 @@ public async Task BeginAsync(string adapterId, AdapterRole role // The subscription's own adapter properties travel with each CALL, not with the // process: this instance is shared by every subscription bound to the data source, // and its startup values are the data source's. Without this a subscription could - // not say which statement to run — it would be reading whatever the data source + // not say which statement to run - it would be reading whatever the data source // was started with, which is the same answer for all of them. return new RunningInstanceSession(running, spec.StartupValues); // Not here. For an EXCLUSIVE data source that is the normal case on every node but - // one — a broker connection is held by a single node so that a queue is drained once. + // one - a broker connection is held by a single node so that a queue is drained once. // // Exclusivity is about CONSUMING, though, not about connecting. A subscription's // handler runs on whichever node picked up the message, so a delivery that publishes - // to the customer's broker would fail on every node but the owner — which is to say, + // to the customer's broker would fail on every node but the owner - which is to say, // almost always. A publish-only connection of our own is the answer: it sends and // never subscribes, so nothing is consumed twice. if (role is AdapterRole.Handler or AdapterRole.Mapper) @@ -99,8 +102,8 @@ public async Task BeginAsync(string adapterId, AdapterRole role var publishing = await PublishOnlySpecAsync(adapterId, dataSourceId); if (publishing != null) // The same split as the exclusive path: the rented instance's startup values - // are the CONNECTION's, and the slot's own properties — where to publish above - // all — travel with the call. Passing only the spec would send the message to + // are the CONNECTION's, and the slot's own properties - where to publish above + // all - travel with the call. Passing only the spec would send the message to // an endpoint the connection never knew about, which is to say nowhere. return new ResidentAdapterSession( await adapters.RentAsync(publishing), spec.StartupValues); @@ -109,12 +112,12 @@ public async Task BeginAsync(string adapterId, AdapterRole role throw new BitweenException( $"Data source {dataSourceId} is not running on this node, so adapter " + $"'{adapterId}' has no connection to work through. If the data source is " - + "exclusive, another node holds it; if it is per-node, look at its health — " + + "exclusive, another node holds it; if it is per-node, look at its health - " + "the supervisor could not start it here."); } // Rented, not started: the process is already up, so the call costs a round trip rather - // than a launch. Returning the lease is what releases it to the next message — and what + // than a launch. Returning the lease is what releases it to the next message - and what // triggers the reset that clears per-message state between borrowers. return new ResidentAdapterSession(await adapters.RentAsync(spec)); } @@ -127,7 +130,7 @@ public async Task BeginAsync(string adapterId, AdapterRole role /// for the same reason: a second instance that subscribed would drain the customer's queue /// alongside the node that owns it, and the lease exists precisely to stop that. /// - /// Pooled rather than exclusive, so several nodes may hold one at once — which is correct for + /// Pooled rather than exclusive, so several nodes may hold one at once - which is correct for /// publishing and wrong for consuming. The pool key includes a hash of these values, so the /// publish-only instance is a different renter from anything else and cannot be handed the /// consuming one by mistake. @@ -173,7 +176,7 @@ private async Task PublishOnlySpecAsync(string adapterId, string da /// A session against the data source's long-lived instance. Nothing is returned on dispose: /// the instance is not ours to give back, and it must outlive this Xchange to be any use to /// the next one. That also means no per-session reset, so a data source adapter must not keep - /// per-message state in a field — which is the same rule any shared connection follows. + /// per-message state in a field - which is the same rule any shared connection follows. /// private sealed class RunningInstanceSession( ResidentAdapterInstance instance, IDictionary properties) : IAdapterSession diff --git a/SW.Bitween.Api/Services/Adapters/ResidentAdapters.cs b/SW.Bitween.Api/Services/Adapters/ResidentAdapters.cs index 9de1c837..5fae68db 100644 --- a/SW.Bitween.Api/Services/Adapters/ResidentAdapters.cs +++ b/SW.Bitween.Api/Services/Adapters/ResidentAdapters.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Services.Adapters; /// /// It lives here because two services outside the runtime need to ask, and both got the answer /// wrong in the same way: they probed the adapter by SPAWNING it down the classic stdio path. A -/// resident adapter dials out instead of speaking stdio, so that probe never gets an answer — one +/// resident adapter dials out instead of speaking stdio, so that probe never gets an answer - one /// caller failed as "Received null data" and refused to save the subscription at all, the other /// failed closed and masked every property as a secret. Neither failure named a cause. /// diff --git a/SW.Bitween.Api/Services/AggregationJob.cs b/SW.Bitween.Api/Services/AggregationJob.cs index 4985b41a..dd16d052 100644 --- a/SW.Bitween.Api/Services/AggregationJob.cs +++ b/SW.Bitween.Api/Services/AggregationJob.cs @@ -16,6 +16,7 @@ public record AggregationJobParams(int SubscriptionId, string? CronExpression); [ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] public class AggregationJob( BitweenDbContext dbContext, + RunFlagUpdater runFlagUpdater, XchangeService xchangeService, ILogger logger) : IScheduledJob { @@ -26,10 +27,29 @@ public async Task Execute(AggregationJobParams jobParams) if (aggSub == null) return; + // Quartz runs each fire once across the cluster, but each schedule of a subscription and each + // "aggregate now" is a job of its own, so two runs of one aggregation can still overlap, on one + // replica or two. Only one roll-up could be saved (an exchange is rolled up once), so the other + // run failed and was logged as an error. Claimed as a receiver is, the other run steps aside. + if (!await runFlagUpdater.MarkAsRunning(aggSub.Id)) return; + try + { + await RollUpAsync(aggSub); + } + finally + { + // Released after the roll-up is saved, so a run that starts next sees it. + await runFlagUpdater.MarkAsIdle(aggSub.Id); + } + } + + async Task RollUpAsync(Subscription aggSub) + { + var startedOn = DateTime.UtcNow; // Decided as the run goes and written once at the end. Recording inside both the try // and the catch wrote two rows for a single run whenever something threw after the - // roll-up was already built — SetSchedules does exactly that on an aggregation that + // roll-up was already built - SetSchedules does exactly that on an aggregation that // somehow has no schedule. var outcome = ReceiveOutcome.NoNewData; string errorMessage = null; @@ -44,8 +64,11 @@ join agg in dbContext.Set() on xchange.Id equals agg.Id into from agg in xa.DefaultIfEmpty() where result.Success == true && agg == null && xchange.SubscriptionId == aggSub.AggregationForId && !aggSub.Inactive + orderby xchange.StartedOn select new { xchange.Id, xchange.FilesPrefix }; + // Oldest first: without an order, a backlog over the limit was rolled up in whatever order + // the database returned, and the same old exchanges could wait run after run. var targetXchangeList = await xchangeQuery.Take(10000).ToListAsync(); if (targetXchangeList.Count > 0) @@ -75,7 +98,7 @@ from agg in xa.DefaultIfEmpty() // that is bookkeeping, and calling the run failed would misreport delivered work. // The error is kept on the attempt either way, and on the integration's health. if (createdExchangeIds.Length == 0) outcome = ReceiveOutcome.Failed; - logger.LogError(ex, "Error processing aggregation for subscription {SubscriptionId}", jobParams.SubscriptionId); + logger.LogError(ex, "Error processing aggregation for subscription {SubscriptionId}", aggSub.Id); } RecordAttempt(aggSub.Id, startedOn, outcome, errorMessage, createdExchangeIds); diff --git a/SW.Bitween.Api/Services/AutoPauseAlertService.cs b/SW.Bitween.Api/Services/AutoPauseAlertService.cs new file mode 100644 index 00000000..895347a5 --- /dev/null +++ b/SW.Bitween.Api/Services/AutoPauseAlertService.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// What the alert handler is sent when a subscription pauses itself. +public class SubscriptionAutoPausedNotification +{ + public string Kind => "SubscriptionAutoPaused"; + public int SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public int Failures { get; set; } + public string XchangeId { get; set; } + public string Exception { get; set; } + public DateTime OccurredOn { get; set; } +} + +/// +/// Tells someone a subscription paused itself, through the alert handler of its retry policy - the +/// place already set up to hear that deliveries stopped going through. A subscription with no such +/// policy, or one with no alert handler, has the pause logged and shown on its page, and nothing sent. +/// +public class AutoPauseAlertService( + BitweenDbContext dbContext, + IAdapterInvoker adapterInvoker, + ILogger logger) : IConsume +{ + public async Task Process(SubscriptionAutoPausedEvent message) + { + // At-least-once delivery: a successful row says it already went out. + var alreadySent = await dbContext.Set() + .AnyAsync(n => n.XchangeId == message.XchangeId + && n.NotifierName == XchangeNotification.AutoPauseAlertName + && n.Success); + if (alreadySent) return; + + var subscription = await dbContext.Set().AsNoTracking() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == message.SubscriptionId); + var target = RetryAlertResolver.Resolve(null, null, subscription?.RetryPolicy); + if (target == null) return; + + var notification = new SubscriptionAutoPausedNotification + { + SubscriptionId = message.SubscriptionId, + SubscriptionName = subscription!.Name, + Failures = message.Failures, + XchangeId = message.XchangeId, + Exception = subscription.LastException, + OccurredOn = message.OccurredOn + }; + var handlerProperties = new Dictionary( + target.HandlerProperties ?? new Dictionary()) + { + ["xchangeid"] = message.XchangeId + }; + + try + { + await adapterInvoker.InvokeAsync( + target.HandlerId, AdapterRole.Handler, nameof(IInfolinkHandler.Handle), + new XchangeFile(JsonConvert.SerializeObject(notification), message.XchangeId), + handlerProperties, message.XchangeId); + dbContext.Add(XchangeNotification.ForAutoPauseAlert(message.XchangeId)); + } + catch (Exception ex) + { + logger.LogError(ex, "Auto-pause alert for subscription {SubscriptionId} could not be delivered through {HandlerId}.", + message.SubscriptionId, target.HandlerId); + dbContext.Add(XchangeNotification.ForAutoPauseAlert(message.XchangeId, ex.ToString())); + } + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Services/BitweenInfo.cs b/SW.Bitween.Api/Services/BitweenInfo.cs new file mode 100644 index 00000000..ff113752 --- /dev/null +++ b/SW.Bitween.Api/Services/BitweenInfo.cs @@ -0,0 +1,29 @@ +using System; + +namespace SW.Bitween; + +/// +/// This Bitween's version, for an adapter manifest's compatibility.minBitweenVersion. +/// +/// +/// The assemblies are built as 1.0.0, so the release cannot be read from them. A deployment's +/// pipeline can set Bitween:Version; otherwise the baseline here stands, raised by hand with +/// each release that adds something an adapter might depend on. +/// +public static class BitweenInfo +{ + public const string Baseline = "10.0.0"; + + public static Version Version(BitweenOptions options) => + System.Version.TryParse(options?.Version?.Split('+', '-')[0], out var configured) + ? configured + : System.Version.Parse(Baseline); + + /// True when is empty, unreadable, or not above this Bitween. + public static bool Satisfies(string minimum, BitweenOptions options) + { + if (string.IsNullOrWhiteSpace(minimum)) return true; + var core = minimum.Trim().TrimStart('v', 'V').Split('+', '-')[0]; + return !System.Version.TryParse(core, out var required) || required <= Version(options); + } +} diff --git a/SW.Bitween.Api/Services/BitweenOptions.cs b/SW.Bitween.Api/Services/BitweenOptions.cs index 1f6e868f..f8e8711d 100644 --- a/SW.Bitween.Api/Services/BitweenOptions.cs +++ b/SW.Bitween.Api/Services/BitweenOptions.cs @@ -14,6 +14,7 @@ public BitweenOptions() DatabaseType = "MySql"; AdminDatabaseName = "defaultdb"; ServerlessCommandTimeout = 300; + NativeAdapterTimeout = 900; BusProvidersEnabled = false; BusProviderMaxInFlight = 16; InboundMessagePruneCron = "0 30 3 * * ?"; @@ -52,6 +53,49 @@ public BitweenOptions() /// /// public bool ExposeApiDocs { get; set; } + + /// + /// Refuse HTTP adapter calls to private and loopback addresses (10/8, 172.16/12, + /// 192.168/16, 127/8, fc00::/7 and the like). Off by default: Bitween is self-hosted, and + /// calling systems inside the customer's own network is a normal integration. Link-local + /// addresses, where the cloud metadata endpoints live, are refused either way. + /// + public bool BlockPrivateNetworkAddresses { get; set; } + + /// + /// Comma-separated addresses or CIDR ranges of the proxies whose X-Forwarded-For is + /// believed. Empty trusts the nearest proxy whatever its address, which is right behind an + /// ingress whose address isn't known ahead of time; set it when clients can also reach the + /// service directly, or they can choose the address they are rate-limited as. + /// + public string TrustedProxies { get; set; } + + /// + /// How long a receiver run may hold its subscription's running flag before another run may + /// take it over. A run killed with its process never clears the flag; this is what lets the + /// subscription run again. Longer than any real run, so a slow one is not run twice. + /// + public int StaleRunAfterMinutes { get; set; } = 120; + + /// + /// The longest a synchronous gateway call is held open waiting for its exchange's result, + /// whatever wait the caller asks for. After it the caller gets 202 with the exchange id. + /// + public int MaxResponseWaitSeconds { get; set; } = 300; + + /// + /// The longest a retry chain may grow, whatever its policy allows. A policy with no total + /// limit used to retry a failure that never recovers for ever. + /// + public int MaxRetryChainDepth { get; set; } = 100; + + /// + /// After a notifier sends for a subscription and outcome, further notifications for the same + /// pair within this many minutes are recorded as suppressed rather than sent, and the next + /// one sent says how many there were. An outage no longer means one email per failure. + /// Zero sends every one. + /// + public int NotifierQuietMinutes { get; set; } = 5; /// /// Where new exchange files are written in storage. The bucket's lifecycle rule for this prefix /// decides how long they're kept (temp30/… is 30 days). Each exchange records the prefix @@ -89,7 +133,57 @@ public BitweenOptions() public int ServerlessCommandTimeout { get; set; } /// - /// Runs resident data source providers on this node — brokers and databases alike. + /// Seconds one call to a built-in adapter may take before it fails as a timeout, a retry policy's to + /// retry. Built-in adapters run in Bitween's own process, so a call that never returns (a server that + /// accepts a connection and says nothing) held its place in the processing queue for good, and enough + /// of them stopped all processing. 0 for no limit. + /// + public int NativeAdapterTimeout { get; set; } + + /// + /// Failures in a row (timeouts, refused or dropped connections, 429, 5xx but 501) after which a + /// built-in adapter's calls to that destination fail at once, as a 503, for + /// . A partner that stopped answering held a place in the + /// processing queue for each delivery's whole timeout, and flows to healthy partners waited behind + /// it. Counted per destination and per replica. 0 switches it off. + /// + public int CircuitBreakerFailures { get; set; } = 5; + + /// The failures must fall within this many seconds to open the circuit. + public int CircuitBreakerWindowSeconds { get; set; } = 60; + + /// Seconds an open circuit fails calls at once before it lets one through to try. + public int CircuitBreakerOpenSeconds { get; set; } = 60; + + /// + /// Lets the adapter editor build drafts that name packages in requirements.txt or + /// package.json: the server then downloads them from PyPI and npm, or from the mirrors + /// PIP_INDEX_URL and NPM_CONFIG_REGISTRY name. Off by default, since it needs the server to + /// reach a package index. Only plain names and versions are accepted, wheels only for Python + /// and no install scripts for Node, so nothing a package brings runs while it is built. + /// + public bool AdapterEditorDependencies { get; set; } + + /// + /// Memory, in megabytes, a draft may use while the adapter editor checks or tries it on the + /// server; past it the run is stopped and says so. 0 for no limit. + /// + public int AdapterEditorMemoryMb { get; set; } = 256; + + /// + /// Cores a draft may keep busy while the editor checks or tries it, held for a few seconds + /// before it is stopped. 0 for no limit. + /// + public double AdapterEditorCpuCores { get; set; } = 1; + + /// + /// This deployment's release, e.g. 10.0.4 - set by a pipeline that knows it. Adapters may + /// declare the lowest Bitween they work with; see . + /// + public string Version { get; set; } + + /// + /// Runs resident data source providers on this node - brokers and databases alike. /// /// Named for brokers because it predates database sources; renaming it would break every /// deployment that already sets it, so operator-facing messages say what it gates rather @@ -149,7 +243,7 @@ public BitweenOptions() /// /// Passphrase used to encrypt secret settings before they're stored. Environment-only and - /// never itself a setting — it's what protects the table, so it can't live in it. Without + /// never itself a setting - it's what protects the table, so it can't live in it. Without /// it, secret settings are neither imported nor editable and keep coming from configuration. /// Rotating it makes anything already stored unreadable. /// @@ -187,10 +281,31 @@ public BitweenOptions() public int ReceiveAttemptRetentionDays { get; set; } = 30; /// - /// Quartz cron expression for ReceiveAttemptCleanupJob. Defaults to daily at 3am — + /// Quartz cron expression for ReceiveAttemptCleanupJob. Defaults to daily at 3am - /// offset from the scheduler library's own cleanup job (2am) so they don't run at once. /// Format: second minute hour dayOfMonth month dayOfWeek /// public string ReceiveAttemptCleanupCron { get; set; } = "0 0 3 * * ?"; + + /// How many days of hourly metrics to keep. A year and a bit, for "same week last year". + public int MetricsRetentionDays { get; set; } = 400; + + /// + /// How many days of exchange history to fold into the metrics once, after the upgrade that adds + /// them, a few hours at a time. 0 skips it. + /// + public int MetricsBackfillDays { get; set; } = 30; + + /// When to delete metrics and broker snapshots past their keeping. Nightly. + public string MetricsPruneCron { get; set; } = "0 15 3 * * ?"; + + /// How often to record the broker's queue totals for the backlog trend. Every five minutes. + public string QueueSnapshotCron { get; set; } = "0 0/5 * * * ?"; + + /// + /// How often to look for subscriptions that expect traffic and have gone quiet. Every five + /// minutes, which is also the shortest gap a subscription can expect. + /// + public string QuietSubscriptionsCron { get; set; } = "0 0/5 * * * ?"; } } \ No newline at end of file diff --git a/SW.Bitween.Api/Services/BitweenTelemetry.cs b/SW.Bitween.Api/Services/BitweenTelemetry.cs new file mode 100644 index 00000000..f60167e1 --- /dev/null +++ b/SW.Bitween.Api/Services/BitweenTelemetry.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace SW.Bitween; + +/// +/// The metrics and traces Bitween emits about its own work. Logs said what happened to one +/// exchange; nothing said how many were flowing, how many failed or how long they took, so a +/// slowdown or a rising failure rate was found by a partner rather than by a dashboard. +/// +/// +/// Recording costs next to nothing when nobody listens. They are exported only when an +/// OpenTelemetry endpoint is configured (OTEL_EXPORTER_OTLP_ENDPOINT); see Startup. +/// +public static class BitweenTelemetry +{ + public const string Name = "Bitween"; + + public static readonly ActivitySource ActivitySource = new(Name); + + private static readonly Meter Meter = new(Name); + + /// Exchanges created, tagged with how they arrived. + public static readonly Counter ExchangesCreated = + Meter.CreateCounter("bitween.exchanges.created", description: "Exchanges created"); + + /// Exchanges processed, tagged outcome: success, bad_response or error. + public static readonly Counter ExchangesProcessed = + Meter.CreateCounter("bitween.exchanges.processed", description: "Exchanges processed, by outcome"); + + /// Time from picking an exchange off the queue to its result being saved. + public static readonly Histogram ProcessingDuration = + Meter.CreateHistogram("bitween.exchanges.processing.duration", unit: "s", + description: "Time to process one exchange"); + + /// Subscriptions paused for failing too many deliveries in a row. + public static readonly Counter SubscriptionsAutoPaused = + Meter.CreateCounter("bitween.subscriptions.auto_paused", + description: "Subscriptions paused after failing too many deliveries in a row"); +} diff --git a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs index 9d0844a4..0837a0ad 100644 --- a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs +++ b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs @@ -35,7 +35,7 @@ public class InMemoryBitweenCache(IMemoryCache memoryCache, IServiceScopeFactory /// /// The six reads take time, and runs on the bus consumer thread against /// this same singleton. A revoke landing between the reads and the writes is revoking the - /// snapshot those reads just took — the write it announces happened after they began — so + /// snapshot those reads just took - the write it announces happened after they began - so /// publishing it anyway would reinstate the staleness the revoke existed to clear, for the /// full ten minutes. Hence the generation check: read again rather than publish a snapshot /// that is already known to be behind. @@ -158,7 +158,7 @@ public async Task DocumentByNameAsync(string documentName) public async Task BroadcastRevoke() { // This is a best-effort cache-refresh signal, not part of the write - // itself — a dozen command handlers across Subscriptions/WorkGroups/ + // itself - a dozen command handlers across Subscriptions/WorkGroups/ // BusGateways/Documents call this right after SaveChangesAsync, and an // unhandled failure here (e.g. the RabbitMQ connection being down) // must not turn an already-successful write into a 500 response. diff --git a/SW.Bitween.Api/Services/CliSignInCodes.cs b/SW.Bitween.Api/Services/CliSignInCodes.cs new file mode 100644 index 00000000..407c7a97 --- /dev/null +++ b/SW.Bitween.Api/Services/CliSignInCodes.cs @@ -0,0 +1,84 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Configuration; + +namespace SW.Bitween; + +/// +/// The one-time codes that sign the bitween CLI in through a browser. A member signed in to the +/// admin UI, however they signed in, asks for a code for the CLI's challenge; the CLI trades it, +/// with the verifier only it holds, for a session of its own (PKCE, as OAuth's loopback flow does). +/// +/// +/// Signed rather than stored, so any node can redeem a code another issued and nothing needs a +/// table: the key is derived from Token:Key, which every node shares, and is not the key access +/// tokens are signed with, so a code can never pass as one. A code is good for two minutes and only +/// with the verifier, which never leaves the CLI; one seen in a browser's history or a proxy's log +/// is no use to whoever saw it. +/// +public class CliSignInCodes(IConfiguration configuration) +{ + public static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(2); + const string Version = "c1"; + + byte[] key; + + byte[] Key => key ??= SHA256.HashData(Encoding.UTF8.GetBytes("bitween-cli-sign-in:" + + (configuration["Token:Key"] ?? throw new InvalidOperationException("Token:Key is not set.")))); + + /// A code for this account and the CLI's S256 challenge. + public string Issue(int accountId, string challenge, DateTimeOffset now) + { + if (!IsChallenge(challenge)) throw new ArgumentException("Not an S256 code challenge.", nameof(challenge)); + var payload = $"{Version}.{accountId}.{(now + Lifetime).ToUnixTimeSeconds()}.{challenge}." + + Base64Url(RandomNumberGenerator.GetBytes(12)); + return Base64Url(Encoding.UTF8.GetBytes(payload)) + "." + Base64Url(Sign(payload)); + } + + /// The account a code was issued for, when it is genuine, unexpired and the verifier matches. + public int? Redeem(string code, string verifier, DateTimeOffset now) + { + if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(verifier) || verifier.Length is < 43 or > 128) return null; + var dot = code.IndexOf('.'); + if (dot <= 0) return null; + + string payload; + byte[] signature; + try + { + payload = Encoding.UTF8.GetString(FromBase64Url(code[..dot])); + signature = FromBase64Url(code[(dot + 1)..]); + } + catch (FormatException) + { + return null; + } + if (!CryptographicOperations.FixedTimeEquals(signature, Sign(payload))) return null; + + var parts = payload.Split('.'); + if (parts.Length != 5 || parts[0] != Version) return null; + if (!int.TryParse(parts[1], out var accountId) || !long.TryParse(parts[2], out var expires)) return null; + if (now.ToUnixTimeSeconds() > expires) return null; + + var expected = Encoding.ASCII.GetBytes(parts[3]); + var actual = Encoding.ASCII.GetBytes(Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)))); + return CryptographicOperations.FixedTimeEquals(expected, actual) ? accountId : null; + } + + /// An S256 challenge: the unpadded base64url of a SHA-256, 43 characters. + public static bool IsChallenge(string challenge) => + challenge is { Length: 43 } && challenge.AsSpan().IndexOfAnyExcept( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") < 0; + + byte[] Sign(string payload) => HMACSHA256.HashData(Key, Encoding.UTF8.GetBytes(payload)); + + static string Base64Url(byte[] bytes) => + Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + static byte[] FromBase64Url(string text) + { + var s = text.Replace('-', '+').Replace('_', '/'); + return Convert.FromBase64String(s + new string('=', (4 - s.Length % 4) % 4)); + } +} diff --git a/SW.Bitween.Api/Services/Cluster/ILeaderElection.cs b/SW.Bitween.Api/Services/Cluster/ILeaderElection.cs index 57f2ce08..1f1b2fbb 100644 --- a/SW.Bitween.Api/Services/Cluster/ILeaderElection.cs +++ b/SW.Bitween.Api/Services/Cluster/ILeaderElection.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.Services.Cluster; /// Exclusive ownership of one named resource across the cluster. /// /// Deliberately an abstraction over ONE implementation. The mechanism today is a RabbitMQ -/// exclusive queue, because the internal bus is RabbitMQ everywhere Bitween runs — but the day +/// exclusive queue, because the internal bus is RabbitMQ everywhere Bitween runs - but the day /// that stops being true, the supervisor should not have to change. What must not happen is two /// implementations maintained at once; the interface exists so the second can replace the first, /// not sit beside it. @@ -33,8 +33,8 @@ public interface IResourceLease : IAsyncDisposable /// /// The fencing token: monotonic, and issued by the database rather than by the lock. /// - /// The lock alone is not enough. A node can be paused long enough — a stop-the-world GC, a - /// network partition that heals — for its exclusive queue to be released and claimed by + /// The lock alone is not enough. A node can be paused long enough - a stop-the-world GC, a + /// network partition that heals - for its exclusive queue to be released and claimed by /// another node while it still believes it holds ownership. RabbitMQ has no monotonic counter /// to detect that, so the database supplies one: acquiring bumps the term, and a holder whose /// term is no longer the current one has been superseded and must stop immediately. diff --git a/SW.Bitween.Api/Services/Cluster/NodeHeartbeat.cs b/SW.Bitween.Api/Services/Cluster/NodeHeartbeat.cs new file mode 100644 index 00000000..824a265c --- /dev/null +++ b/SW.Bitween.Api/Services/Cluster/NodeHeartbeat.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain.Cluster; +using SW.Serverless.Runtimes; + +namespace SW.Bitween.Services.Cluster; + +/// +/// Writes this node into ClusterNodes every , so any node can say which +/// nodes there are, which run data sources and which can run which adapters. A node not seen for +/// is shown as gone; one not seen for a week is forgotten. +/// +public class NodeHeartbeat(IServiceScopeFactory scopes, BitweenOptions options, IServiceProvider services, + ILogger logger) : BackgroundService +{ + public static readonly TimeSpan Interval = TimeSpan.FromSeconds(30); + public static readonly TimeSpan GoneAfter = TimeSpan.FromSeconds(90); + static readonly TimeSpan ForgetAfter = TimeSpan.FromDays(7); + + /// This node's name, as leases record their owner: host and process id. + public static string NodeName { get; } = $"{Environment.MachineName}:{Environment.ProcessId}"; + + static readonly DateTime StartedOn = DateTime.UtcNow; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + string runtimes = null; + while (!stoppingToken.IsCancellationRequested) + { + try + { + runtimes ??= await RuntimesAsync(); + await BeatAsync(runtimes, stoppingToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // The node works without it; only the Nodes view is the poorer for a missed beat. + logger.LogWarning(ex, "Couldn't record this node's heartbeat."); + } + + try { await Task.Delay(Interval, stoppingToken); } + catch (OperationCanceledException) { break; } + } + } + + async Task BeatAsync(string runtimes, CancellationToken cancellation) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var now = DateTime.UtcNow; + + var node = await db.Set().FindAsync([NodeName], cancellation); + if (node == null) db.Add(node = new ClusterNode(NodeName)); + node.Host = Environment.MachineName; + node.StartedOn = StartedOn; + node.LastSeenOn = now; + node.Version = BitweenInfo.Version(options).ToString(); + node.DataSources = options.BusProvidersEnabled; + node.Runtimes = runtimes; + node.ResidentHealth = ResidentHealthJson(); + await db.SaveChangesAsync(cancellation); + + var forgotten = now - ForgetAfter; + await db.Set().Where(n => n.LastSeenOn < forgotten).ExecuteDeleteAsync(cancellation); + } + + /// + /// The resident adapter instances on this node, as JSON for the Nodes page: what each is doing, + /// what it uses, how often it has been restarted. Null without any. Kept within the column by + /// listing at most , busiest first. + /// + internal const int MaxInstances = 25; + + string ResidentHealthJson() + { + try + { + var host = services.GetService(); + var instances = host?.Describe(); + if (instances == null || instances.Count == 0) return null; + var rows = instances + .OrderByDescending(i => i.InFlight).ThenByDescending(i => i.WorkingSetBytes) + .Take(MaxInstances) + .Select(i => new + { + a = i.AdapterId, + s = i.State.ToString(), + pid = i.ProcessId, + mb = (int)(i.WorkingSetBytes / 1024 / 1024), + cpu = Math.Round(i.CpuPercent, 1), + up = (long)i.Uptime.TotalSeconds, + r = i.RestartCount, + q = i.Quarantined, + f = i.InFlight, + e = i.LastError?.Length > 120 ? i.LastError[..120] : i.LastError, + }); + var json = System.Text.Json.JsonSerializer.Serialize(rows); + return json.Length <= 4000 ? json : null; + } + catch + { + return null; + } + } + + async Task RuntimesAsync() + { + var detector = services.GetService(); + if (detector == null) return ""; + var available = await Task.WhenAll(new[] { "dotnet", "python", "node" } + .Select(async name => (await detector.StatusAsync(name)).Available ? name : null)); + return string.Join(",", available.Where(n => n != null)); + } +} diff --git a/SW.Bitween.Api/Services/Cluster/RabbitMqLeaderElection.cs b/SW.Bitween.Api/Services/Cluster/RabbitMqLeaderElection.cs index 5a3b34e5..87357aed 100644 --- a/SW.Bitween.Api/Services/Cluster/RabbitMqLeaderElection.cs +++ b/SW.Bitween.Api/Services/Cluster/RabbitMqLeaderElection.cs @@ -14,13 +14,13 @@ namespace SW.Bitween.Services.Cluster; /// /// Leader election over the internal RabbitMQ, using the one primitive it already gives us: /// an EXCLUSIVE QUEUE IS TIED TO A SINGLE CONNECTION. Declaring it succeeds for exactly one node -/// and fails for every other, and the broker releases it the moment that connection goes — which +/// and fails for every other, and the broker releases it the moment that connection goes - which /// is liveness for free, with no lease renewal to get wrong and no clock to trust. /// /// The database supplies what the broker cannot: a monotonic term. See . /// /// The connection here is DEDICATED and separate from SW.Bus's. Sharing it would tie every lease -/// in the process to the same fate as ordinary message traffic — one connection blip would drop +/// in the process to the same fate as ordinary message traffic - one connection blip would drop /// every broker connection this node owns, all at once. /// public class RabbitMqLeaderElection : ILeaderElection, IDisposable @@ -43,7 +43,7 @@ public RabbitMqLeaderElection(IConfiguration configuration, IServiceProvider ser // Distinct per process, not per machine: two instances on one host must not believe they // are the same owner. - nodeName = $"{Environment.MachineName}:{Environment.ProcessId}"; + nodeName = NodeHeartbeat.NodeName; var connectionString = configuration.GetConnectionString("RabbitMQ") ?? throw new InvalidOperationException( @@ -55,7 +55,7 @@ public RabbitMqLeaderElection(IConfiguration configuration, IServiceProvider ser Uri = new Uri(connectionString), // Recovery must stay OFF. A recovered connection silently re-declares the exclusive // queue, so a node that lost ownership during an outage would quietly take it back - // without ever bumping the term — two owners, neither aware. + // without ever bumping the term - two owners, neither aware. AutomaticRecoveryEnabled = false, RequestedConnectionTimeout = TimeSpan.FromSeconds(15) }; @@ -80,7 +80,7 @@ public async Task TryAcquireAsync(string resource, } catch (OperationInterruptedException) { - // RESOURCE_LOCKED — someone else owns it. Ordinary, and not worth logging above debug. + // RESOURCE_LOCKED - someone else owns it. Ordinary, and not worth logging above debug. channel?.Dispose(); _logger.LogDebug("Resource {Resource} is owned by another node.", resource); return null; @@ -94,7 +94,7 @@ public async Task TryAcquireAsync(string resource, try { - // THE FENCE, and only after the lock is held — so exactly one node bumps the term. + // THE FENCE, and only after the lock is held - so exactly one node bumps the term. var term = await ClaimTermAsync(resource, cancellationToken); _logger.LogInformation("Node {Node} acquired {Resource} at term {Term}.", @@ -181,7 +181,7 @@ private sealed class RabbitMqLease(string resource, string queue, long term, IMo public string Resource { get; } = resource; public long Term { get; } = term; - // The channel closing IS the loss of ownership — the broker has already released the queue. + // The channel closing IS the loss of ownership - the broker has already released the queue. public bool IsHeld => !_released && _channel.IsOpen; public async Task ValidateAsync(CancellationToken cancellationToken = default) @@ -205,7 +205,7 @@ public ValueTask DisposeAsync() // DELETE the queue, do not merely close the channel. // - // An exclusive queue belongs to the CONNECTION, not the channel — AMQP deletes it when + // An exclusive queue belongs to the CONNECTION, not the channel - AMQP deletes it when // the connection closes. Closing the channel released nothing, so a node that gave up // a lease still held the lock until its whole election connection dropped, and the // next node could never take over. Deleting is allowed for the owning connection and diff --git a/SW.Bitween.Api/Services/DataSources/BitweenAdapterStateStore.cs b/SW.Bitween.Api/Services/DataSources/BitweenAdapterStateStore.cs index 81831ce4..7ab5a47c 100644 --- a/SW.Bitween.Api/Services/DataSources/BitweenAdapterStateStore.cs +++ b/SW.Bitween.Api/Services/DataSources/BitweenAdapterStateStore.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Services.DataSources; /// -/// Where a resident adapter's bookmarks actually live — the durable replacement for the in-memory +/// Where a resident adapter's bookmarks actually live - the durable replacement for the in-memory /// store SW-Serverless registers by default. /// /// It has to be durable and it has to be shared. A polling database receiver saves the cursor it @@ -18,7 +18,7 @@ namespace SW.Bitween.Services.DataSources; /// processed, and a data source that moves to another node replays everything. Neither is a /// tolerable answer for a receiver whose whole job is to read each row once. /// -/// Registered as a SINGLETON, because the resident host is one — hence the scope created per call +/// Registered as a SINGLETON, because the resident host is one - hence the scope created per call /// rather than an injected . /// public class BitweenAdapterStateStore(IServiceProvider serviceProvider) : IAdapterStateStore @@ -42,8 +42,8 @@ public async Task SetAsync(AdapterStateKey key, string value, CancellationToken if (value != null && value.Length > AdapterState.MaxValueLength) throw new SWException( $"Adapter state '{key.Name}' is {value.Length} characters, and the limit is " - + $"{AdapterState.MaxValueLength}. This holds a bookmark — a cursor, a watermark, a " - + "small JSON object of them — not data an adapter is staging."); + + $"{AdapterState.MaxValueLength}. This holds a bookmark - a cursor, a watermark, a " + + "small JSON object of them - not data an adapter is staging."); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); diff --git a/SW.Bitween.Api/Services/DataSources/BusProviderEventSink.cs b/SW.Bitween.Api/Services/DataSources/BusProviderEventSink.cs index f4bd1fdc..46c657ae 100644 --- a/SW.Bitween.Api/Services/DataSources/BusProviderEventSink.cs +++ b/SW.Bitween.Api/Services/DataSources/BusProviderEventSink.cs @@ -18,7 +18,7 @@ namespace SW.Bitween.Services.DataSources; /// Where a message from an external broker enters Bitween. /// /// The adapter owns the connection and hands the payload over; everything after that is the path -/// an internally-bussed message already takes — , +/// an internally-bussed message already takes - , /// which persists the Xchange, writes the payload to cloud storage and lets the filter decide /// which subscriptions run. Ingress from a broker and ingress from the API are the same thing /// past this point, deliberately. @@ -43,14 +43,14 @@ public async Task OnEventAsync(InboundEvent inboundEvent, Cancella return EventOutcome.Rejected($"'{inboundEvent.InstanceKey}' is not a data source id."); // Which gateway on this data source owns this endpoint. Endpoint is what the adapter was - // told to consume — a queue, a topic, an SQS URL. + // told to consume - a queue, a topic, an SQS URL. var gateway = await dbContext.Set() .Where(g => g.DataSourceId == dataSourceId && !g.Inactive) .Where(g => g.Endpoint == inboundEvent.Endpoint || g.Endpoint == null) // An exact endpoint match beats the catch-all. Ordering by the endpoint itself does // NOT express that: PostgreSQL sorts NULLS FIRST on a descending order, so the // catch-all would win every race and every specific endpoint's messages would be - // filed against the wrong Document. Order by the match itself instead — true first, + // filed against the wrong Document. Order by the match itself instead - true first, // and it means the same thing on all three providers. .OrderByDescending(g => g.Endpoint == inboundEvent.Endpoint) .FirstOrDefaultAsync(cancellationToken); @@ -120,7 +120,7 @@ await xchangeService.SubmitFilterXchange( /// /// Namespaced by data source, always. The adapters already qualify their keys by host and - /// queue, but an SP-API key is just the notification id — globally unique, so two data sources + /// queue, but an SP-API key is just the notification id - globally unique, so two data sources /// subscribed to the same notification would silently deduplicate against each other. That /// might occasionally be wanted; it should never be something you get by accident. /// diff --git a/SW.Bitween.Api/Services/DataSources/BusProviderSupervisor.cs b/SW.Bitween.Api/Services/DataSources/BusProviderSupervisor.cs index 9f330338..bd106520 100644 --- a/SW.Bitween.Api/Services/DataSources/BusProviderSupervisor.cs +++ b/SW.Bitween.Api/Services/DataSources/BusProviderSupervisor.cs @@ -20,16 +20,16 @@ namespace SW.Bitween.Services.DataSources; /// /// Desired state is the set of active rows; actual state is what the /// resident host is running. The loop starts what is missing, stops what is no longer wanted, and -/// restarts what has changed — the same shape as the Quartz schedule reconciliation that already +/// restarts what has changed - the same shape as the Quartz schedule reconciliation that already /// exists for subscriptions. /// -/// PLACEMENT. A broker connection is exclusive — two nodes consuming the same queue is duplicate +/// PLACEMENT. A broker connection is exclusive - two nodes consuming the same queue is duplicate /// processing, which is the failure this whole design exists to prevent. So every data source is /// owned through a lease, granted per data source rather than globally: whichever node wins each /// race owns that source, so load spreads without anyone scheduling it. /// /// A lease is checked, not assumed. Before every reconcile the supervisor revalidates what it -/// believes it owns, because holding the lock is not the same as still being the current owner — +/// believes it owns, because holding the lock is not the same as still being the current owner - /// a node paused long enough for its queue to be released and reclaimed would otherwise carry on /// consuming. Losing a lease stops its adapter immediately. /// @@ -44,6 +44,16 @@ public class BusProviderSupervisor(IServiceProvider serviceProvider, IResidentAd // What this node currently owns. Nothing runs without an entry here. private readonly Dictionary _leases = new(); + /// + /// Consecutive failed starts per data source on this node, and when this node may next try to + /// lease one it gave up. A node that cannot reach a broker kept the lease and retried forever, + /// so the replica that could reach it never got the chance. + /// + private readonly Dictionary _startFailures = new(); + private readonly Dictionary _yieldedUntil = new(); + private const int StartFailuresBeforeYielding = 3; + private static readonly TimeSpan YieldFor = TimeSpan.FromMinutes(5); + private static string ResourceOf(DataSource dataSource) => $"datasource.{dataSource.Id}"; protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -80,8 +90,8 @@ public override async Task StopAsync(CancellationToken cancellationToken) } /// - /// One reconciliation pass. Public because "reconcile now" is a real operation — the same - /// shape as the receivenow endpoint for subscriptions — and because a supervisor whose only + /// One reconciliation pass. Public because "reconcile now" is a real operation - the same + /// shape as the receivenow endpoint for subscriptions - and because a supervisor whose only /// entry point is a thirty-second timer cannot be tested at all. /// public async Task ReconcileAsync(CancellationToken cancellationToken = default) @@ -102,7 +112,7 @@ public async Task ReconcileAsync(CancellationToken cancellationToken = default) .ToListAsync(cancellationToken); // Composed into the Statements value the adapter has always received. The adapter contract - // did not change when these moved out of a JSON field on the data source and into rows — + // did not change when these moved out of a JSON field on the data source and into rows - // it still resolves a name and knows nothing about where the SQL is kept. var statements = await dbContext.Set() .Where(s => !s.Inactive) @@ -115,6 +125,10 @@ public async Task ReconcileAsync(CancellationToken cancellationToken = default) foreach (var dataSource in desired) { + if (!_leases.ContainsKey(dataSource.Id) && + _yieldedUntil.TryGetValue(dataSource.Id, out var yieldedUntil) && yieldedUntil > DateTime.UtcNow) + continue; + if (!await EnsureOwnedAsync(dataSource, cancellationToken)) { // Owned by another node. If we were running it, we are not any more. @@ -154,6 +168,8 @@ await adapters.StartExclusiveAsync(new AdapterSpec }, cancellationToken); _running[dataSource.Id] = fingerprint; + _startFailures.Remove(dataSource.Id); + _yieldedUntil.Remove(dataSource.Id); logger.LogInformation("Data source {Name} running on adapter {AdapterId}.", dataSource.Name, dataSource.AdapterId); } @@ -167,10 +183,24 @@ await adapters.StartExclusiveAsync(new AdapterSpec // log. A start that throws leaves no instance to describe, so nothing else in this // class would ever record it. await RecordStartFailureAsync(dbContext, dataSource, ex, cancellationToken); + + // An exclusive source is held by one node at a time. If this node keeps failing to + // start it - it can't reach the broker, say - let it go for a while so another + // replica can try; a single-node deployment just retries after the pause. + var failures = _startFailures[dataSource.Id] = _startFailures.GetValueOrDefault(dataSource.Id) + 1; + if (failures >= StartFailuresBeforeYielding && _leases.ContainsKey(dataSource.Id)) + { + logger.LogWarning( + "Data source {Name} failed to start {Failures} times on this node; releasing its lease for {Minutes} minutes so another node can take it.", + dataSource.Name, failures, YieldFor.TotalMinutes); + await ReleaseAsync(dataSource.Id); + _startFailures.Remove(dataSource.Id); + _yieldedUntil[dataSource.Id] = DateTime.UtcNow + YieldFor; + } } } - // Anything running that is no longer desired. Its lease goes too — holding a lock on a + // Anything running that is no longer desired. Its lease goes too - holding a lock on a // data source nobody wants would block a node that later does. var wanted = desired.Select(d => d.Id).ToHashSet(); foreach (var id in _running.Keys.Where(k => !wanted.Contains(k)).ToList()) @@ -258,7 +288,7 @@ private async Task ReleaseAsync(int dataSourceId) /// /// Connection settings from the data source, plus the endpoints its gateways want consumed. - /// The adapter decides what to do with them — Bitween does not model any broker's topology. + /// The adapter decides what to do with them - Bitween does not model any broker's topology. /// private static Dictionary BuildStartupValues(DataSource dataSource, IEnumerable gateways, IEnumerable statements) @@ -277,7 +307,7 @@ private static Dictionary BuildStartupValues(DataSource dataSour // Per-gateway overrides, namespaced so they cannot collide with connection settings. // See BusGateway.EndpointProperties: no bundled adapter reads these yet. Skipping the - // catch-all gateway is deliberate — an override with no endpoint to attach to would land + // catch-all gateway is deliberate - an override with no endpoint to attach to would land // under the meaningless key "Endpoint::something". foreach (var gateway in mine.Where(g => !string.IsNullOrWhiteSpace(g.Endpoint))) foreach (var kv in gateway.EndpointProperties ?? new()) @@ -298,7 +328,7 @@ private static long Megabytes(int megabytes) => /// /// What the adapter was started WITH. The memory ceilings belong in here as much as the /// connection settings do: they are applied to the process at launch, so raising one has no - /// effect until the adapter restarts — and the fingerprint is what decides that it should. + /// effect until the adapter restarts - and the fingerprint is what decides that it should. /// private static string Fingerprint(DataSource dataSource, Dictionary startupValues) => dataSource.AdapterId + "|" + @@ -310,7 +340,7 @@ private static string Fingerprint(DataSource dataSource, Dictionary @@ -344,7 +374,7 @@ private async Task RecordStartFailureAsync(BitweenDbContext dbContext, DataSourc private async Task WriteBackHealthAsync(BitweenDbContext dbContext, CancellationToken cancellationToken) { // Only the instances that ARE a data source. Describe() answers for everything this host - // holds, and a POOLED instance is keyed by its pool slot rather than by a data source — + // holds, and a POOLED instance is keyed by its pool slot rather than by a data source - // several can share one key, which made a plain ToDictionary throw "an item with the same // key has already been added" and take the whole reconcile pass down with it. // @@ -375,14 +405,14 @@ private async Task WriteBackHealthAsync(BitweenDbContext dbContext, Cancellation // LastError is what a LIVE adapter says about itself, so it is null for exactly the // failure an operator most needs explained: one that died on startup and never got as // far as reporting anything. The process printed why before it exited, so fall back to - // that — otherwise the screen shows "Quarantined" with no reason anywhere near it. + // that - otherwise the screen shows "Quarantined" with no reason anywhere near it. row.LastException = instance.LastError ?? AdapterFailureReader.Summarise( adapters.Get(row.AdapterId, row.Id.ToString())?.Diagnostics); row.ConsecutiveFailures = instance.RestartCount; // A per-node source has no owner and saying "null" would read as nobody running it. - // Its row is written by every node, last one wins — which is honest enough for a + // Its row is written by every node, last one wins - which is honest enough for a // summary, and the Telemetry endpoint answers per node for anything more precise. row.OwnedByNode = row.Resolve() == DataSourcePlacement.PerNode ? "every node" diff --git a/SW.Bitween.Api/Services/DataSources/DataSourceProviderCatalog.cs b/SW.Bitween.Api/Services/DataSources/DataSourceProviderCatalog.cs index 0f9e438b..bd07a9ad 100644 --- a/SW.Bitween.Api/Services/DataSources/DataSourceProviderCatalog.cs +++ b/SW.Bitween.Api/Services/DataSources/DataSourceProviderCatalog.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.Services.DataSources; /// /// What each data source provider accepts, read out of the adapter itself. /// -/// The front end used to carry this list by hand — which fields a RabbitMQ data source starts with, +/// The front end used to carry this list by hand - which fields a RabbitMQ data source starts with, /// what DeclareMode allows, which values are credentials. A hand-written copy of someone else's /// contract drifts: the form advertised a "Tls" field for months that the adapter never read, so an /// operator could tick it, see nothing wrong, and still be authenticating in the clear. @@ -28,8 +28,8 @@ namespace SW.Bitween.Services.DataSources; /// one would mean an extra connection to a customer's broker, and asking a stopped one would mean /// starting a connection nobody asked for. /// - The attributes are matched by NAME, not by type identity. Adapters build against their own -/// copy of the contract — they target net8.0 while the host is on net10.0 — and a type loaded in -/// a metadata context is never reference-equal to the one the host compiled against anyway. +/// copy of the contract, published on their own cadence, and a type loaded in a metadata +/// context is never reference-equal to the one the host compiled against anyway. /// public class DataSourceProviderCatalog(AdapterInstaller installer, ICloudFilesService cloudFiles, ServerlessOptions options, IMemoryCache cache, ILogger logger) @@ -37,7 +37,7 @@ public class DataSourceProviderCatalog(AdapterInstaller installer, ICloudFilesSe /// /// Where Bitween's own providers live. A third-party adapter is found by the Kind stamped on /// it at publish time instead; this prefix is what finds the ones published before stamping - /// existed, and it is a prefilter either way — describing every adapter in the store to build + /// existed, and it is a prefilter either way - describing every adapter in the store to build /// a menu would mean downloading every adapter in the store. /// public const string ConventionPrefix = "bitween."; diff --git a/SW.Bitween.Api/Services/DataSources/InboundMessagePruneJob.cs b/SW.Bitween.Api/Services/DataSources/InboundMessagePruneJob.cs index 33a5a7cc..fa36651e 100644 --- a/SW.Bitween.Api/Services/DataSources/InboundMessagePruneJob.cs +++ b/SW.Bitween.Api/Services/DataSources/InboundMessagePruneJob.cs @@ -12,7 +12,7 @@ namespace SW.Bitween.Services.DataSources; /// /// Forgets dedupe keys once they are older than their data source's window. /// -/// The table only ever grows otherwise — one row per inbound message, for ever. Pruning per data +/// The table only ever grows otherwise - one row per inbound message, for ever. Pruning per data /// source rather than on one global age matters because the window is a property of the customer's /// broker: a queue with a seven-day message TTL and one that can be replayed from a dead-letter /// months later do not want the same answer. diff --git a/SW.Bitween.Api/Services/DataSources/StatementComposer.cs b/SW.Bitween.Api/Services/DataSources/StatementComposer.cs index 529e111f..f478541b 100644 --- a/SW.Bitween.Api/Services/DataSources/StatementComposer.cs +++ b/SW.Bitween.Api/Services/DataSources/StatementComposer.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.Services.DataSources; /// This is the seam that let statements become their own entity without touching the adapter /// contract: the adapter has always been handed name-to-SQL as JSON and has never known whether /// that came from a field somebody typed into or from rows with their own permissions and audit -/// trail. Keeping the composition here — rather than inline in the supervisor — is what makes it +/// trail. Keeping the composition here - rather than inline in the supervisor - is what makes it /// testable without starting a database. /// public static class StatementComposer @@ -41,7 +41,7 @@ public static string Compose(IEnumerable statements, int da } /// - /// A bare SQL string, unless the statement carries the columns a receiver needs — in which + /// A bare SQL string, unless the statement carries the columns a receiver needs - in which /// case an object. /// /// Both shapes on purpose. A statement that is only ever queried composes to exactly the diff --git a/SW.Bitween.Api/Services/DataSources/StatementUsageReader.cs b/SW.Bitween.Api/Services/DataSources/StatementUsageReader.cs index 96309cac..e31426c2 100644 --- a/SW.Bitween.Api/Services/DataSources/StatementUsageReader.cs +++ b/SW.Bitween.Api/Services/DataSources/StatementUsageReader.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.Services.DataSources; /// Which subscriptions name which statements. /// /// Answering it needs a scan rather than a join, because a subscription names a statement inside -/// its adapter properties — a JSON column — and those are not queryable in a way that is the same +/// its adapter properties - a JSON column - and those are not queryable in a way that is the same /// on PostgreSQL, MySQL and SQL Server. The scan is bounded by the subscriptions bound to ONE data /// source, which is a small number by construction, and it happens on a screen rather than in the /// message path. @@ -28,7 +28,7 @@ public class StatementUsageReader(BitweenDbContext dbContext) /// Must match what the adapter reads: a handler runs Statement, a receiver polls with /// ReceiveStatement and marks rows with MarkProcessedStatement. /// - /// Missing one here does not fail loudly — it reports a statement that is in daily use as + /// Missing one here does not fail loudly - it reports a statement that is in daily use as /// unused, which is exactly the licence to delete it that the usage count exists to withhold. /// static readonly (string Key, string Usage)[] StatementKeys = @@ -43,7 +43,7 @@ static readonly (string Key, string Usage)[] StatementKeys = public const string OperationKey = "Operation"; /// - /// Usage for every statement of one data source, keyed by statement name, case-insensitively — + /// Usage for every statement of one data source, keyed by statement name, case-insensitively - /// the adapter resolves names that way, so counting them any other way would report a statement /// as unused while a subscription happily runs it. /// @@ -73,8 +73,8 @@ static void Record(Dictionary> usage { if (properties == null) return; - // One slot can name two statements — a receiver polls with one and marks rows with - // another — so each is recorded separately, and the operation says which job it does. + // One slot can name two statements - a receiver polls with one and marks rows with + // another - so each is recorded separately, and the operation says which job it does. foreach (var (key, usageVerb) in StatementKeys) { var name = Value(properties, key); diff --git a/SW.Bitween.Api/Services/DataSources/StatementValidator.cs b/SW.Bitween.Api/Services/DataSources/StatementValidator.cs index bb429773..1eceb12b 100644 --- a/SW.Bitween.Api/Services/DataSources/StatementValidator.cs +++ b/SW.Bitween.Api/Services/DataSources/StatementValidator.cs @@ -11,19 +11,19 @@ namespace SW.Bitween.Services.DataSources; /// /// Asks the database whether a statement is valid, at the moment it is saved. /// -/// The alternative is what we had: store anything, and find out at the connection test — or, if +/// The alternative is what we had: store anything, and find out at the connection test - or, if /// nobody ran one, on the first message, as a failed Xchange pointing at a syntax error made days /// earlier by someone else. A typo belongs to whoever typed it, and the only time that is true is /// while they are still looking at the form. /// /// It PREPARES the SQL: parsed and planned by the engine, never executed, nothing stored. That is -/// the same check the connection test runs, deliberately — two checks that could disagree about +/// the same check the connection test runs, deliberately - two checks that could disagree about /// what is acceptable would be worse than one. /// /// It is best-effort by construction. The adapter has to be running on THIS node to answer, and /// for a relational source it is (a pool is held per node), but a source that is stopped, broken /// or still starting cannot be asked. In that case the save proceeds unchecked rather than being -/// blocked by an unrelated fault — refusing to let someone fix a statement because the connection +/// blocked by an unrelated fault - refusing to let someone fix a statement because the connection /// they are fixing it for is down would be exactly backwards. /// public class StatementValidator( @@ -70,7 +70,7 @@ public async Task ValidateAsync(int dataSourceId, string sql) } catch (Exception) { - // The adapter could not be reached, or does not know the command — an older package, + // The adapter could not be reached, or does not know the command - an older package, // or a provider that never implemented it. Either way this is a fact about the // adapter, not about the SQL, and it must not stop someone saving. return Result.NotChecked; diff --git a/SW.Bitween.Api/Services/DocumentSchema.cs b/SW.Bitween.Api/Services/DocumentSchema.cs new file mode 100644 index 00000000..fe8b0f49 --- /dev/null +++ b/SW.Bitween.Api/Services/DocumentSchema.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Schema; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NJsonSchema; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// A document that does not match the schema of its information type. +public class DocumentSchemaException(string documentName, IReadOnlyList errors) + : Exception($"The document does not match the schema of '{documentName}':\n- " + string.Join("\n- ", errors)) +{ + public IReadOnlyList Errors { get; } = errors; +} + +/// +/// The schema an information type can carry - JSON Schema for a JSON type, XSD for an XML one - +/// and checking a document against it. +/// +/// +/// A schema may only refer to itself: a $ref to a URL or file, or an XSD import or include, +/// would have the server fetch whatever an editor typed in. Compiled schemas are kept by their +/// text, so an edit is simply a new entry. +/// +public static class DocumentSchema +{ + public const int MaxErrors = 20; + + /// The property the older partner endpoint adds to what it was sent. Not the partner's. + private const string InjectedProperty = "_ExternalRequestContext"; + + private static readonly ConcurrentDictionary>> Compiled = new(); + + /// Refuses a schema that can't be used for the format; null or empty is no schema. + public static async Task EnsureUsable(DocumentFormat format, string schema) + { + if (string.IsNullOrWhiteSpace(schema)) return; + if (format is not (DocumentFormat.Json or DocumentFormat.Xml)) + throw new SWValidationException("SCHEMA_NOT_SUPPORTED", + "Only JSON and XML information types can carry a schema."); + try + { + await CompileAsync(format, schema); + } + catch (Exception ex) when (ex is not SWValidationException) + { + throw new SWValidationException("INVALID_SCHEMA", + (format == DocumentFormat.Json ? "Not a usable JSON Schema: " : "Not a usable XSD: ") + ex.Message); + } + } + + /// What is wrong with , at most ; empty when it matches. + public static async Task> Check(DocumentFormat format, string schema, string data) + { + if (string.IsNullOrWhiteSpace(schema) || format is not (DocumentFormat.Json or DocumentFormat.Xml)) + return []; + + var compiled = await CompileAsync(format, schema); + return format == DocumentFormat.Json + ? CheckJson((JsonSchema)compiled, data) + : CheckXml((XmlSchemaSet)compiled, data); + } + + /// Throws when the document doesn't match. + public static async Task Enforce(Domain.Document document, string data) + { + if (document is null) return; + var errors = await Check(document.DocumentFormat, document.ValidationSchema, data); + if (errors.Count > 0) throw new DocumentSchemaException(document.Name, errors); + } + + private static async Task CompileAsync(DocumentFormat format, string schema) + { + if (Compiled.Count > 1000) Compiled.Clear(); + var key = $"{format}:{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(schema)))}"; + var entry = Compiled.GetOrAdd(key, _ => new Lazy>(async () => format == DocumentFormat.Json + ? await CompileJson(schema) + : CompileXml(schema))); + try + { + return await entry.Value; + } + catch + { + // A schema that failed to compile is not kept, so fixing it is not answered from the cache. + Compiled.TryRemove(key, out _); + throw; + } + } + + private static async Task CompileJson(string schema) => + await JsonSchema.FromJsonAsync(schema, null, + root => new LocalOnlyResolver(new JsonSchemaAppender(root, new DefaultTypeNameGenerator())), + CancellationToken.None); + + private static XmlSchemaSet CompileXml(string schema) + { + var set = new XmlSchemaSet { XmlResolver = null }; + using var reader = XmlReader.Create(new StringReader(schema), + new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }); + var compiled = XmlSchema.Read(reader, null)!; + if (compiled.Includes.Count > 0) + throw new SWValidationException("INVALID_SCHEMA", + "The XSD imports or includes another schema. Put everything it needs in this one."); + set.Add(compiled); + set.Compile(); + return set; + } + + private static IReadOnlyList CheckJson(JsonSchema schema, string data) + { + JToken token; + try + { + token = JToken.Parse(data); + } + catch (JsonReaderException ex) + { + return [$"Not valid JSON: {ex.Message}"]; + } + + if (token is JObject obj && obj.ContainsKey(InjectedProperty)) + { + obj = (JObject)obj.DeepClone(); + obj.Remove(InjectedProperty); + token = obj; + } + + return schema.Validate(token).SelectMany(Flatten).Take(MaxErrors).ToList(); + + static IEnumerable Flatten(NJsonSchema.Validation.ValidationError error) => + error is NJsonSchema.Validation.ChildSchemaValidationError child && child.Errors.Count > 0 + ? child.Errors.SelectMany(e => e.Value).SelectMany(Flatten) + : [$"{(string.IsNullOrEmpty(error.Path) ? "#" : error.Path)}: {error.Kind}"]; + } + + private static IReadOnlyList CheckXml(XmlSchemaSet schemas, string data) + { + var errors = new List(); + var settings = new XmlReaderSettings + { + ValidationType = ValidationType.Schema, + Schemas = schemas, + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }; + settings.ValidationEventHandler += (_, e) => + { + if (errors.Count < MaxErrors) + errors.Add($"line {e.Exception?.LineNumber}, position {e.Exception?.LinePosition}: {e.Message}"); + }; + + // An XmlSchemaSet is not safe to share between threads. + lock (schemas) + { + try + { + using var reader = XmlReader.Create(new StringReader(data), settings); + while (reader.Read()) + { + } + } + catch (XmlException ex) + { + errors.Add($"Not valid XML: {ex.Message}"); + } + } + + return errors; + } + + private class LocalOnlyResolver(JsonSchemaAppender appender) : JsonReferenceResolver(appender) + { + public override Task ResolveFileReferenceAsync(string filePath, + CancellationToken cancellationToken = default) => + throw new SWValidationException("INVALID_SCHEMA", + $"The schema refers to '{filePath}'. Put everything it needs in this one."); + + public override Task ResolveUrlReferenceAsync(string url, + CancellationToken cancellationToken = default) => + throw new SWValidationException("INVALID_SCHEMA", + $"The schema refers to '{url}'. Put everything it needs in this one."); + } +} diff --git a/SW.Bitween.Api/Services/FileLinks.cs b/SW.Bitween.Api/Services/FileLinks.cs index 756655bf..6f383a83 100644 --- a/SW.Bitween.Api/Services/FileLinks.cs +++ b/SW.Bitween.Api/Services/FileLinks.cs @@ -19,7 +19,7 @@ namespace SW.Bitween; /// /// Exchange files are private in storage, so a storage URL no longer opens. A link here carries a seal /// made from the file's storage key with a server secret: it opens that one file, and changing any -/// character of it — to reach another exchange — breaks the seal. Nothing is stored per link. +/// character of it - to reach another exchange - breaks the seal. Nothing is stored per link. /// /// /// A link lives as long as its file, like the public storage URL it replaces. The secret is derived @@ -46,7 +46,7 @@ public class FileLinks(BitweenOptions options, IConfiguration configuration, IHt /// /// The address readers reach Bitween on: the public address the chart sets, or else the address the - /// current request came in on, or else — a scheduled job — . + /// current request came in on, or else - a scheduled job - . /// public string BaseUrl { @@ -64,7 +64,7 @@ public string BaseUrl /// /// The address this instance listens on, as the adapters it runs reach it: they run beside it, in /// the same container, so a wildcard binding (http://+:8080) is localhost, and plain - /// HTTP is preferred — no certificate for them to trust. Nothing outside the instance can open a + /// HTTP is preferred - no certificate for them to trust. Nothing outside the instance can open a /// link on it. null with no server. /// public string InstanceUrl @@ -85,7 +85,7 @@ public string InstanceUrl /// /// The address a roll-up's links are built on: the public address the chart sets, since its handler may - /// pass them to a partner, or else — never a request's, which is wherever the + /// pass them to a partner, or else - never a request's, which is wherever the /// person who clicked happened to be. /// public string AdapterBaseUrl => diff --git a/SW.Bitween.Api/Services/FilterService.cs b/SW.Bitween.Api/Services/FilterService.cs index aaa1b337..456ab79d 100644 --- a/SW.Bitween.Api/Services/FilterService.cs +++ b/SW.Bitween.Api/Services/FilterService.cs @@ -49,7 +49,7 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) catch (Exception ex) when (ex is JsonException || ex is XPathException) { // A path the reader cannot parse throws out of it rather than returning false, - // and unhandled it reached the caller as a bare 400 naming nothing — on every + // and unhandled it reached the caller as a bare 400 naming nothing - on every // message of this information type, since the bad path is on the type itself. // Which property is broken is the one thing needed to fix it. throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", @@ -61,7 +61,7 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) // returning false, leaving the property null. Only an unreadable path throws. // Stored as the payload sent it. It used to be lower-cased here, which was - // only ever to pair with the lower-cased term in Xchanges/Search — nothing + // only ever to pair with the lower-cased term in Xchanges/Search - nothing // matches on this dictionary (match expressions read the payload directly), // so the one thing it changed was what every screen displays: an order for // "Acme Retail" listed as "acme retail". Search now lower-cases the column @@ -74,15 +74,15 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) var matches = subs.Where(sub => { // An integration with an entry point of its own is never started by a document - // merely arriving on its information type — it is started through that entry + // merely arriving on its information type - it is started through that entry // point, which is what decides it should run at all: // - // BusGateway — its gateway's routes (route filter + optional partner) - // Receiving — its schedule, via ReceivingJob - // GatewayApiCall — a partner calling the gateway it is attached to - // ApiCall — its own partner posting to Xchanges/Update, which runs the + // BusGateway - its gateway's routes (route filter + optional partner) + // Receiving - its schedule, via ReceivingJob + // GatewayApiCall - a partner calling the gateway it is attached to + // ApiCall - its own partner posting to Xchanges/Update, which runs the // subscription belonging to the caller (legacy GatewayApiCall) - // Response — the subscription whose delivery response it is handed + // Response - the subscription whose delivery response it is handed // // All five are started by name. Auto-matching them as well ran them a second time, // on traffic addressed to nobody: a scheduled job publishing the very message type @@ -93,7 +93,7 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) // so every {{partner.…}} in its adapters stayed a literal token. // // Internal keeps matching. Reacting to a document of its type arriving is the - // whole definition of the type — it has no other trigger. Aggregation is driven + // whole definition of the type - it has no other trigger. Aggregation is driven // by AggregationJob. if (sub.Type is SubscriptionType.BusGateway or SubscriptionType.Receiving diff --git a/SW.Bitween.Api/Services/GatewayActivity.cs b/SW.Bitween.Api/Services/GatewayActivity.cs new file mode 100644 index 00000000..dc4c88ea --- /dev/null +++ b/SW.Bitween.Api/Services/GatewayActivity.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; + +namespace SW.Bitween; + +/// +/// What partners' calls to gateways did that an operator can't otherwise see: when each API key was +/// last used, and the calls turned away. "The partner says they get 401" had nothing to look at. +/// +/// +/// Refusals are kept in memory, per node, since the node started: writing each to the database +/// would let anyone with a gateway's address make Bitween write as fast as they could call it. Key +/// use is written to the database, but at most every per key and +/// node, so a busy key costs one small write every few minutes. +/// +public class GatewayActivity(IServiceScopeFactory scopes, ILogger logger, SW.Bitween.Services.Metrics.MetricsRecorder metrics = null) +{ + public static readonly TimeSpan KeyUseResolution = TimeSpan.FromMinutes(5); + const int Kept = 500; + + public DateTime Since { get; } = DateTime.UtcNow; + + public record Refusal(DateTime On, string Gateway, string Reason, int Status, string Address); + + readonly ConcurrentQueue recent = new(); + readonly ConcurrentDictionary<(string Gateway, string Reason), long> counts = new(); + readonly ConcurrentDictionary<(int PartnerId, string KeyName), DateTime> written = new(); + + /// A call to (its url name) turned away, and why. + public void Refused(string gateway, string reason, int status, string address) + { + gateway = (gateway ?? "").Trim('/').ToLowerInvariant(); + counts.AddOrUpdate((gateway, reason), 1, (_, n) => n + 1); + // Kept past a restart and across nodes too, in the hourly metrics; the counts above are this + // node's since it started, for the gateway page's live view. + metrics?.Refusal(gateway, reason); + recent.Enqueue(new Refusal(DateTime.UtcNow, gateway, reason, status, address)); + while (recent.Count > Kept && recent.TryDequeue(out _)) { } + } + + public (IReadOnlyDictionary Counts, IReadOnlyList Recent) RefusalsOf(string gateway, int take = 25) + { + gateway = (gateway ?? "").Trim('/').ToLowerInvariant(); + return (counts.Where(c => c.Key.Gateway == gateway).ToDictionary(c => c.Key.Reason, c => c.Value), + recent.Where(r => r.Gateway == gateway).OrderByDescending(r => r.On).Take(take).ToList()); + } + + /// Records that a key got a call through, unless this node did so moments ago. + public async Task KeyUsedAsync(int partnerId, string keyName) + { + if (string.IsNullOrEmpty(keyName)) return; + var now = DateTime.UtcNow; + var slot = (partnerId, keyName); + if (written.TryGetValue(slot, out var last) && now - last < KeyUseResolution) return; + written[slot] = now; + + try + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var updated = await db.Set() + .Where(u => u.PartnerId == partnerId && u.KeyName == keyName) + .ExecuteUpdateAsync(u => u.SetProperty(x => x.LastUsedOn, now)); + if (updated == 0) + { + db.Add(new ApiKeyUse(partnerId, keyName, now)); + await db.SaveChangesAsync(); + } + } + catch (Exception ex) + { + // Another node may have added the row first, or the database is busy: the call went + // through either way, and the next one after the resolution tries again. + written.TryRemove(slot, out _); + logger.LogDebug(ex, "Couldn't record a use of partner {PartnerId}'s key {KeyName}.", partnerId, keyName); + } + } +} diff --git a/SW.Bitween.Api/Services/GatewayCallers.cs b/SW.Bitween.Api/Services/GatewayCallers.cs index 3ccfd85e..74f0c497 100644 --- a/SW.Bitween.Api/Services/GatewayCallers.cs +++ b/SW.Bitween.Api/Services/GatewayCallers.cs @@ -36,19 +36,19 @@ public class GatewayCallers( ]; /// The partner, and the reference the exchange records for who called. - public async Task<(bool Authorized, Partner Partner, string Reference)> Identify( + public async Task<(bool Authorized, Partner Partner, string Reference, string KeyName)> Identify( ApiGateway gateway, RequestContext requestContext) { if (gateway.AuthMethod != GatewayAuthMethod.Jwt) { var (authorized, keyHolder, keyName) = await dbContext.CheckPartnerAuthorized(requestContext, gateway.PartnerKeyHeader ?? options.PartnerKeyHeader); - return (authorized, keyHolder, $"partnerkey: {keyName}"); + return (authorized, keyHolder, $"partnerkey: {keyName}", keyName); } var identity = await IdentityFromToken(gateway, requestContext); if (identity == null) - return (false, null, null); + return (false, null, null, null); var partner = await dbContext.Set().AsNoTracking() .SingleOrDefaultAsync(p => p.LoginIdentity == identity); @@ -57,10 +57,10 @@ public class GatewayCallers( // A genuine token, so most likely a partner whose identity hasn't been set yet. logger.LogInformation("Token on gateway {Gateway} names {Identity}, which no partner has", gateway.UrlName, identity); - return (false, null, null); + return (false, null, null, null); } - return (true, partner, $"jwt: {identity}"); + return (true, partner, $"jwt: {identity}", null); } async Task IdentityFromToken(ApiGateway gateway, RequestContext requestContext) @@ -91,7 +91,7 @@ async Task IdentityFromToken(ApiGateway gateway, RequestContext requestC ValidIssuer = gateway.JwtIssuer, ValidAudience = gateway.JwtAudience, // Keys come from the login server, and are fetched again when a token is signed with - // one not seen yet — which is what a login server rotating its keys looks like here. + // one not seen yet - which is what a login server rotating its keys looks like here. ConfigurationManager = keys, ValidAlgorithms = SigningAlgorithms, RequireSignedTokens = true, diff --git a/SW.Bitween.Api/Services/GatewayIssuers.cs b/SW.Bitween.Api/Services/GatewayIssuers.cs index 77b6c306..f6a1d9f2 100644 --- a/SW.Bitween.Api/Services/GatewayIssuers.cs +++ b/SW.Bitween.Api/Services/GatewayIssuers.cs @@ -21,7 +21,7 @@ public interface IGatewayIssuers public class OpenIdGatewayIssuers : IGatewayIssuers { // Until a login server's keys have been read once, every call waits on reading them. The - // default timeout is 100 seconds — a partner call held that long by a firewall dropping the + // default timeout is 100 seconds - a partner call held that long by a firewall dropping the // connection, rather than refused in a few. static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(10) }; @@ -41,7 +41,7 @@ public BaseConfigurationManager For(string issuer) => byIssuer.GetOrAdd(issuer, /// /// A full https address. Plain http only on this machine, for trying a gateway out against a - /// login server running locally — anywhere else the keys could be swapped on the way. + /// login server running locally - anywhere else the keys could be swapped on the way. /// public static bool IsUsable(string issuer) => Uri.TryCreate(issuer, UriKind.Absolute, out var uri) diff --git a/SW.Bitween.Api/Services/IdempotencyGuard.cs b/SW.Bitween.Api/Services/IdempotencyGuard.cs new file mode 100644 index 00000000..70f91b75 --- /dev/null +++ b/SW.Bitween.Api/Services/IdempotencyGuard.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; + +namespace SW.Bitween; + +/// +/// Honours an Idempotency-Key sent with a partner's call: within the window, the same key from the +/// same caller gets the exchange the first call created rather than a new one. +/// +public static class IdempotencyGuard +{ + public const string Header = "Idempotency-Key"; + public const int MaxKeyLength = 200; + public static readonly TimeSpan Window = TimeSpan.FromHours(24); + + private static DateTime _nextPrune = DateTime.MinValue; + + /// The exchange an earlier call with this key created, while the key is still live. + public static async Task FindAsync(BitweenDbContext dbContext, string scope, string key) + { + var since = DateTime.UtcNow - Window; + return await dbContext.Set().AsNoTracking() + .Where(k => k.Scope == scope && k.Key == key && k.CreatedOn > since) + .Select(k => k.XchangeId) + .FirstOrDefaultAsync(); + } + + /// + /// Stages the key with the exchange about to be saved, so the two commit together. A key past + /// its window is removed first: the unique index would otherwise refuse it for good. + /// + public static async Task StageAsync(BitweenDbContext dbContext, string scope, string key, string xchangeId) + { + var since = DateTime.UtcNow - Window; + await dbContext.Set() + .Where(k => k.Scope == scope && k.Key == key && k.CreatedOn <= since) + .ExecuteDeleteAsync(); + + // Old keys go now and then, rather than through a job of their own. + if (DateTime.UtcNow >= _nextPrune) + { + _nextPrune = DateTime.UtcNow.AddHours(1); + var expired = DateTime.UtcNow - Window - TimeSpan.FromHours(1); + await dbContext.Set().Where(k => k.CreatedOn < expired).ExecuteDeleteAsync(); + } + + dbContext.Add(new IdempotencyKey(scope, key, xchangeId)); + } + + /// True when a save failed because another call with the same key committed first. + public static bool IsDuplicateKey(DbUpdateException ex) => + ex.Entries.Any(e => e.Entity is IdempotencyKey); +} diff --git a/SW.Bitween.Api/Services/MappingContextFactory.cs b/SW.Bitween.Api/Services/MappingContextFactory.cs index b8a1c94f..da3b9922 100644 --- a/SW.Bitween.Api/Services/MappingContextFactory.cs +++ b/SW.Bitween.Api/Services/MappingContextFactory.cs @@ -18,8 +18,15 @@ namespace SW.Bitween; /// public class MappingContextFactory(BitweenDbContext dbContext, IInfolinkCache cache) { + /// + /// Replace the partner and global values their owners declared secret with + /// . The preview sets it: a mapping the caller + /// writes can copy any value into its output, so an unmasked context would hand every partner's + /// credentials to whoever can open the editor. The pipeline leaves it off, since it has to send + /// the real values. + /// public async Task Build(int? partnerId, string? xchangeId = null, - IReadOnlyDictionary? sourceValues = null) + IReadOnlyDictionary? sourceValues = null, bool maskSecrets = false) { var partner = partnerId.HasValue ? await dbContext.FindAsync(partnerId.Value) @@ -28,11 +35,17 @@ public async Task Build(int? partnerId, string? xchangeId = null var globals = new Dictionary>(); foreach (var set in await cache.ListGlobalAdapterValuesSetsAsync()) if (set.Values?.Count > 0) - globals[set.Id] = set.Values; + globals[set.Id] = maskSecrets + ? AdapterSecretProperties.Mask(set.Values, set.SecretProperties) + : set.Values; + + var partnerValues = partner?.AdapterProperties ?? new Dictionary(); + if (maskSecrets) + partnerValues = AdapterSecretProperties.Mask(partnerValues, partner?.SecretProperties); return new MappingContext { - Partner = partner?.AdapterProperties ?? new Dictionary(), + Partner = partnerValues, Globals = globals, Source = sourceValues ?? new Dictionary(), XchangeId = xchangeId, diff --git a/SW.Bitween.Api/Services/Metrics/ErrorFingerprint.cs b/SW.Bitween.Api/Services/Metrics/ErrorFingerprint.cs new file mode 100644 index 00000000..cfcf43da --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/ErrorFingerprint.cs @@ -0,0 +1,53 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace SW.Bitween.Services.Metrics; + +/// +/// Groups errors that are the same mistake with different details: the first line of the message, +/// with ids, numbers, dates, addresses and quoted values replaced by a mark, hashed. +/// +public static partial class ErrorFingerprint +{ + public const int SampleLength = 500; + + [GeneratedRegex(@"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")] + private static partial Regex Guid(); + [GeneratedRegex(@"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z?")] + private static partial Regex Timestamp(); + [GeneratedRegex(@"https?://\S+")] + private static partial Regex Url(); + [GeneratedRegex(@"""[^""]*""|'[^']*'")] + private static partial Regex Quoted(); + [GeneratedRegex(@"\b[0-9a-fA-F]{12,}\b")] + private static partial Regex Hex(); + [GeneratedRegex(@"\d+")] + private static partial Regex Number(); + + /// The line worth showing: the first non-empty one, cut to fit. + public static string Sample(string message) + { + if (string.IsNullOrWhiteSpace(message)) return "(no message)"; + foreach (var line in message.Split('\n')) + { + var trimmed = line.Trim(); + if (trimmed.Length > 0) return trimmed.Length <= SampleLength ? trimmed : trimmed[..SampleLength]; + } + return "(no message)"; + } + + public static string Of(string message) + { + var shape = Sample(message); + shape = Guid().Replace(shape, "#"); + shape = Timestamp().Replace(shape, "#"); + shape = Url().Replace(shape, "#"); + shape = Quoted().Replace(shape, "#"); + shape = Hex().Replace(shape, "#"); + shape = Number().Replace(shape, "#"); + var hash = SHA1.HashData(Encoding.UTF8.GetBytes(shape)); + return Convert.ToHexString(hash, 0, 8).ToLowerInvariant(); + } +} diff --git a/SW.Bitween.Api/Services/Metrics/MetricsBackfill.cs b/SW.Bitween.Api/Services/Metrics/MetricsBackfill.cs new file mode 100644 index 00000000..35073285 --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/MetricsBackfill.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Metrics; + +namespace SW.Bitween.Services.Metrics; + +/// +/// Folds the exchanges from before metrics were recorded into the hourly rows, once, so the charts +/// have history from the day of the upgrade and not only from then on. +/// +/// +/// Built to be gentle on a busy database: one hour at a time, newest first (so recent days appear +/// soonest), over the indexed Xchange.StartedOn, streamed rather than loaded, with a pause +/// between hours. Each hour is written with a marker row in one save; if two nodes take the same +/// hour, the key lets one of them write it and the other moves on, so nothing is counted twice. +/// It starts below the hour live recording began in, so the two never overlap. +/// +public class MetricsBackfill(IServiceScopeFactory scopes, BitweenOptions options, ILogger logger) + : BackgroundService +{ + /// Subscription and partner of the row holding the hour live recording began. + public const int CutoffMark = -2; + + public static TimeSpan StartAfter { get; set; } = TimeSpan.FromMinutes(2); + public static TimeSpan PauseBetweenHours { get; set; } = TimeSpan.FromMilliseconds(250); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (options.MetricsBackfillDays <= 0) return; + try + { + await Task.Delay(StartAfter, stoppingToken); + await RunAsync(stoppingToken); + } + catch (OperationCanceledException) + { + // Stopping; what is done stays done, the rest is picked up next start. + } + catch (Exception ex) + { + logger.LogWarning(ex, "Folding exchange history into the metrics stopped; it carries on at the next start."); + } + } + + /// Does the whole backfill now. Public for tests. + public async Task RunAsync(CancellationToken cancellation) + { + var cutoff = await CutoffAsync(cancellation); + var oldest = cutoff.AddDays(-options.MetricsBackfillDays); + for (var hour = cutoff.AddHours(-1); hour >= oldest; hour = hour.AddHours(-1)) + { + cancellation.ThrowIfCancellationRequested(); + if (await FoldHourAsync(hour, cancellation) && PauseBetweenHours > TimeSpan.Zero) + await Task.Delay(PauseBetweenHours, cancellation); + } + } + + /// + /// The hour live recording began in, written the first time any node asks and read ever after: + /// later restarts must not move it, or hours would be counted by both. + /// + async Task CutoffAsync(CancellationToken cancellation) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var mark = await Mark(db).Select(s => (DateTime?)s.Hour).FirstOrDefaultAsync(cancellation); + if (mark != null) return DateTime.SpecifyKind(mark.Value, DateTimeKind.Utc); + + db.Add(new ExchangeStats + { + Hour = MetricsRecorder.HourOf(DateTime.UtcNow), Node = ExchangeStats.HistoryNode, + SubscriptionId = CutoffMark, PartnerId = CutoffMark, + }); + try + { + await db.SaveChangesAsync(cancellation); + } + catch (DbUpdateException) + { + // Another node wrote it first; theirs counts. + } + + await using var again = scopes.CreateAsyncScope(); + var hour = await Mark(again.ServiceProvider.GetRequiredService()) + .Select(s => s.Hour).FirstAsync(cancellation); + return DateTime.SpecifyKind(hour, DateTimeKind.Utc); + } + + static IQueryable Mark(BitweenDbContext db) => + db.Set().AsNoTracking() + .Where(s => s.Node == ExchangeStats.HistoryNode && s.SubscriptionId == CutoffMark && s.PartnerId == CutoffMark) + .OrderBy(s => s.Hour); + + /// Counts one hour of history, unless it is done already. True when it did the work. + async Task FoldHourAsync(DateTime hour, CancellationToken cancellation) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + if (await db.Set().AnyAsync(s => s.Hour == hour && s.Node == ExchangeStats.HistoryNode && + s.SubscriptionId == ExchangeStats.HourDone, cancellation)) + return false; + + var end = hour.AddHours(1); + var rows = new Dictionary<(int, int), ExchangeStats>(); + var finished = + from x in db.Set().AsNoTracking() + where x.StartedOn >= hour && x.StartedOn < end + join r in db.Set().AsNoTracking() on x.Id equals r.Id + select new + { + x.SubscriptionId, x.PartnerId, x.StartedOn, x.InputSize, IsRetry = x.RetryFor != null, x.ManualRetry, + r.Success, r.ResponseBad, r.FinishedOn, r.OutputSize, r.ResponseSize, + }; + await foreach (var e in finished.AsAsyncEnumerable().WithCancellation(cancellation)) + { + var key = (e.SubscriptionId ?? ExchangeStats.None, e.PartnerId ?? ExchangeStats.None); + if (!rows.TryGetValue(key, out var row)) + rows[key] = row = new ExchangeStats + { + Hour = hour, Node = ExchangeStats.HistoryNode, SubscriptionId = key.Item1, PartnerId = key.Item2, + }; + row.Total++; + if (!e.Success) row.Failed++; + else if (e.ResponseBad) row.BadResponse++; + else row.Succeeded++; + if (e.IsRetry) row.Retries++; + if (e.ManualRetry) row.ManualRetries++; + row.BytesIn += e.InputSize; + row.BytesOut += e.OutputSize + e.ResponseSize; + row.AddDuration(Math.Max(0, (long)(e.FinishedOn - e.StartedOn).TotalMilliseconds)); + } + + db.AddRange(rows.Values); + db.Add(new ExchangeStats + { + Hour = hour, Node = ExchangeStats.HistoryNode, SubscriptionId = ExchangeStats.HourDone, PartnerId = ExchangeStats.HourDone, + }); + try + { + await db.SaveChangesAsync(cancellation); + } + catch (DbUpdateException) + { + // Another node folded this hour at the same time and saved first. + } + return true; + } +} diff --git a/SW.Bitween.Api/Services/Metrics/MetricsFlusher.cs b/SW.Bitween.Api/Services/Metrics/MetricsFlusher.cs new file mode 100644 index 00000000..e1e6327d --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/MetricsFlusher.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain.Metrics; + +namespace SW.Bitween.Services.Metrics; + +/// +/// Adds what this node counted into the hourly rows, once a minute and once more on the way down. +/// Per table that is one read of this node's rows for the hours in hand and one save: the cost of +/// metrics to the database is a few small writes a minute per node, however busy the node is. +/// +public class MetricsFlusher(MetricsRecorder recorder, IServiceScopeFactory scopes, ILogger logger) + : BackgroundService +{ + public static readonly TimeSpan Interval = TimeSpan.FromMinutes(1); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try { await Task.Delay(Interval, stoppingToken); } + catch (OperationCanceledException) { break; } + await FlushAsync(CancellationToken.None); + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await base.StopAsync(cancellationToken); + await FlushAsync(cancellationToken); + } + + /// Writes what is counted so far. Public for tests, which can't wait a minute. + public async Task FlushAsync(CancellationToken cancellation) + { + var snapshot = recorder.Drain(); + if (snapshot.IsEmpty) return; + try + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await AddExchangesAsync(db, snapshot, cancellation); + await AddAdaptersAsync(db, snapshot, cancellation); + await AddErrorsAsync(db, snapshot, cancellation); + await db.SaveChangesAsync(cancellation); + } + catch (Exception ex) + { + // Kept for the next flush rather than lost: a database blip costs a minute's delay. + recorder.Restore(snapshot); + logger.LogWarning(ex, "Couldn't write this node's metrics; they are kept for the next try."); + } + } + + static async Task AddExchangesAsync(BitweenDbContext db, MetricsRecorder.Snapshot snapshot, CancellationToken cancellation) + { + if (snapshot.Exchanges.Count == 0) return; + var node = MetricsRecorder.Node; + var hours = snapshot.Exchanges.Select(s => s.Hour).Distinct().ToList(); + var existing = (await db.Set().Where(r => r.Node == node && hours.Contains(r.Hour)).ToListAsync(cancellation)) + .ToDictionary(r => (r.Hour, r.SubscriptionId, r.PartnerId)); + foreach (var counted in snapshot.Exchanges) + if (existing.TryGetValue((counted.Hour, counted.SubscriptionId, counted.PartnerId), out var row)) row.Add(counted); + else db.Add(counted); + } + + static async Task AddAdaptersAsync(BitweenDbContext db, MetricsRecorder.Snapshot snapshot, CancellationToken cancellation) + { + if (snapshot.Adapters.Count == 0) return; + var node = MetricsRecorder.Node; + var hours = snapshot.Adapters.Select(s => s.Hour).Distinct().ToList(); + var existing = (await db.Set().Where(r => r.Node == node && hours.Contains(r.Hour)).ToListAsync(cancellation)) + .ToDictionary(r => (r.Hour, r.AdapterId, r.Version, r.Role, r.Runtime)); + foreach (var counted in snapshot.Adapters) + if (existing.TryGetValue((counted.Hour, counted.AdapterId, counted.Version, counted.Role, counted.Runtime), out var row)) row.Add(counted); + else db.Add(counted); + } + + static async Task AddErrorsAsync(BitweenDbContext db, MetricsRecorder.Snapshot snapshot, CancellationToken cancellation) + { + if (snapshot.Errors.Count == 0) return; + var node = MetricsRecorder.Node; + var hours = snapshot.Errors.Select(s => s.Hour).Distinct().ToList(); + var existing = (await db.Set().Where(r => r.Node == node && hours.Contains(r.Hour)).ToListAsync(cancellation)) + .ToDictionary(r => (r.Hour, r.Scope, r.ScopeKey, r.Fingerprint)); + foreach (var counted in snapshot.Errors) + if (existing.TryGetValue((counted.Hour, counted.Scope, counted.ScopeKey, counted.Fingerprint), out var row)) + { + row.Count += counted.Count; + row.Sample = counted.Sample; + if (counted.LastSeenOn > row.LastSeenOn) row.LastSeenOn = counted.LastSeenOn; + } + else db.Add(counted); + } +} diff --git a/SW.Bitween.Api/Services/Metrics/MetricsPruneJob.cs b/SW.Bitween.Api/Services/Metrics/MetricsPruneJob.cs new file mode 100644 index 00000000..4159a3ff --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/MetricsPruneJob.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Metrics; +using SW.Scheduler; + +namespace SW.Bitween.Services.Metrics; + +/// +/// Deletes hourly metrics older than and broker +/// snapshots older than a month. Each delete is a range on the leading column of the table's key. +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class MetricsPruneJob(BitweenDbContext db, BitweenOptions options) : IScheduledJob +{ + public static readonly TimeSpan SnapshotsKeptFor = TimeSpan.FromDays(31); + + public async Task Execute() + { + var cutoff = MetricsRecorder.HourOf(DateTime.UtcNow).AddDays(-Math.Max(1, options.MetricsRetentionDays)); + // The marker of where recorded history begins stays: without it the backfill would start over. + await db.Set() + .Where(s => s.Hour < cutoff && s.SubscriptionId != MetricsBackfill.CutoffMark) + .ExecuteDeleteAsync(); + await db.Set().Where(s => s.Hour < cutoff).ExecuteDeleteAsync(); + await db.Set().Where(s => s.Hour < cutoff).ExecuteDeleteAsync(); + var snapshots = DateTime.UtcNow - SnapshotsKeptFor; + await db.Set().Where(s => s.At < snapshots).ExecuteDeleteAsync(); + } +} diff --git a/SW.Bitween.Api/Services/Metrics/MetricsQueries.cs b/SW.Bitween.Api/Services/Metrics/MetricsQueries.cs new file mode 100644 index 00000000..cf60da9a --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/MetricsQueries.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using SW.Bitween.Domain.Metrics; + +namespace SW.Bitween.Services.Metrics; + +/// Exchange counts added up over some rows, with the rates and percentiles read off them. +public class ExchangeTotals +{ + public long Total { get; set; } + public long Succeeded { get; set; } + public long BadResponse { get; set; } + public long Failed { get; set; } + public long FailedAtMapper { get; set; } + public long FailedAtHandler { get; set; } + public long Retries { get; set; } + public long ManualRetries { get; set; } + public long BytesIn { get; set; } + public long BytesOut { get; set; } + public long DurationSumMs { get; set; } + public long DurationMaxMs { get; set; } + public long D0 { get; set; } + public long D1 { get; set; } + public long D2 { get; set; } + public long D3 { get; set; } + public long D4 { get; set; } + public long D5 { get; set; } + public long D6 { get; set; } + public long D7 { get; set; } + public long D8 { get; set; } + public long D9 { get; set; } + + public long[] Buckets() => [D0, D1, D2, D3, D4, D5, D6, D7, D8, D9]; + + /// Of the finished exchanges, the share that succeeded (a bad response is not a success). Null without any. + public double? SuccessRate => Total == 0 ? null : Math.Round(100.0 * Succeeded / Total, 2); + public long? P50Ms => DurationBuckets.Percentile(Buckets(), 0.5, DurationMaxMs); + public long? P95Ms => DurationBuckets.Percentile(Buckets(), 0.95, DurationMaxMs); + public long? AverageMs => Total == 0 ? null : DurationSumMs / Total; + + public void Add(ExchangeTotals o) + { + Total += o.Total; Succeeded += o.Succeeded; BadResponse += o.BadResponse; Failed += o.Failed; + FailedAtMapper += o.FailedAtMapper; FailedAtHandler += o.FailedAtHandler; Retries += o.Retries; + ManualRetries += o.ManualRetries; BytesIn += o.BytesIn; BytesOut += o.BytesOut; DurationSumMs += o.DurationSumMs; + DurationMaxMs = Math.Max(DurationMaxMs, o.DurationMaxMs); + D0 += o.D0; D1 += o.D1; D2 += o.D2; D3 += o.D3; D4 += o.D4; D5 += o.D5; D6 += o.D6; D7 += o.D7; D8 += o.D8; D9 += o.D9; + } +} + +public class ExchangeTotals : ExchangeTotals +{ + public TKey Key { get; set; } +} + +/// Adapter call counts added up, with rates and percentiles. +public class AdapterTotals +{ + public long Calls { get; set; } + public long Failures { get; set; } + public long Timeouts { get; set; } + public long LimitStops { get; set; } + public long Crashes { get; set; } + public long DurationSumMs { get; set; } + public long DurationMaxMs { get; set; } + public long D0 { get; set; } + public long D1 { get; set; } + public long D2 { get; set; } + public long D3 { get; set; } + public long D4 { get; set; } + public long D5 { get; set; } + public long D6 { get; set; } + public long D7 { get; set; } + public long D8 { get; set; } + public long D9 { get; set; } + + public long[] Buckets() => [D0, D1, D2, D3, D4, D5, D6, D7, D8, D9]; + public double? FailureRate => Calls == 0 ? null : Math.Round(100.0 * Failures / Calls, 2); + public long? P50Ms => DurationBuckets.Percentile(Buckets(), 0.5, DurationMaxMs); + public long? P95Ms => DurationBuckets.Percentile(Buckets(), 0.95, DurationMaxMs); +} + +public class AdapterTotals : AdapterTotals +{ + public TKey Key { get; set; } +} + +/// +/// The grouped sums every metrics page is made of, computed by the database over the hourly rows. +/// Sums are taken as 64-bit so a busy year can't overflow a 32-bit column total. +/// +public static class MetricsQueries +{ + /// Rows of real counts: without the markers the history backfill keeps. + public static IQueryable Exchanges(BitweenDbContext db, DateTime from, DateTime to) => + db.Set().AsNoTracking().Where(s => s.Hour >= from && s.Hour < to && s.SubscriptionId >= 0); + + public static IQueryable Adapters(BitweenDbContext db, DateTime from, DateTime to) => + db.Set().AsNoTracking().Where(s => s.Hour >= from && s.Hour < to); + + public static Task>> SumBy(this IQueryable> groups) => + groups.Select(g => new ExchangeTotals + { + Key = g.Key, + Total = g.Sum(x => (long)x.Total), + Succeeded = g.Sum(x => (long)x.Succeeded), + BadResponse = g.Sum(x => (long)x.BadResponse), + Failed = g.Sum(x => (long)x.Failed), + FailedAtMapper = g.Sum(x => (long)x.FailedAtMapper), + FailedAtHandler = g.Sum(x => (long)x.FailedAtHandler), + Retries = g.Sum(x => (long)x.Retries), + ManualRetries = g.Sum(x => (long)x.ManualRetries), + BytesIn = g.Sum(x => x.BytesIn), + BytesOut = g.Sum(x => x.BytesOut), + DurationSumMs = g.Sum(x => x.DurationSumMs), + DurationMaxMs = g.Max(x => x.DurationMaxMs), + D0 = g.Sum(x => (long)x.D0), D1 = g.Sum(x => (long)x.D1), D2 = g.Sum(x => (long)x.D2), + D3 = g.Sum(x => (long)x.D3), D4 = g.Sum(x => (long)x.D4), D5 = g.Sum(x => (long)x.D5), + D6 = g.Sum(x => (long)x.D6), D7 = g.Sum(x => (long)x.D7), D8 = g.Sum(x => (long)x.D8), + D9 = g.Sum(x => (long)x.D9), + }).ToListAsync(); + + public static Task>> SumBy(this IQueryable> groups) => + groups.Select(g => new AdapterTotals + { + Key = g.Key, + Calls = g.Sum(x => (long)x.Calls), + Failures = g.Sum(x => (long)x.Failures), + Timeouts = g.Sum(x => (long)x.Timeouts), + LimitStops = g.Sum(x => (long)x.LimitStops), + Crashes = g.Sum(x => (long)x.Crashes), + DurationSumMs = g.Sum(x => x.DurationSumMs), + DurationMaxMs = g.Max(x => x.DurationMaxMs), + D0 = g.Sum(x => (long)x.D0), D1 = g.Sum(x => (long)x.D1), D2 = g.Sum(x => (long)x.D2), + D3 = g.Sum(x => (long)x.D3), D4 = g.Sum(x => (long)x.D4), D5 = g.Sum(x => (long)x.D5), + D6 = g.Sum(x => (long)x.D6), D7 = g.Sum(x => (long)x.D7), D8 = g.Sum(x => (long)x.D8), + D9 = g.Sum(x => (long)x.D9), + }).ToListAsync(); + + public static int Days(int? days) => Math.Clamp(days ?? 7, 1, 90); + + public static DateTime Utc(DateTime value) => DateTime.SpecifyKind(value, DateTimeKind.Utc); + + /// A minute is as fresh as the counts themselves: they are added once a minute. + public static Task Cached(this IMemoryCache cache, string key, Func> compute) => + cache.GetOrCreateAsync($"bitween.metrics.{key}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); + return compute(); + })!; + + /// + /// When the metrics begin: the oldest hour folded in from history, or failing that the hour live + /// recording started. Pages say "since" this rather than show an empty past as zero traffic. + /// + public static async Task SinceAsync(BitweenDbContext db) + { + var oldest = await db.Set().AsNoTracking() + .OrderBy(s => s.Hour).Select(s => (DateTime?)s.Hour).FirstOrDefaultAsync(); + return oldest == null ? null : Utc(oldest.Value); + } + + public static async Task AdapterSinceAsync(BitweenDbContext db) + { + var oldest = await db.Set().AsNoTracking() + .OrderBy(s => s.Hour).Select(s => (DateTime?)s.Hour).FirstOrDefaultAsync(); + return oldest == null ? null : Utc(oldest.Value); + } +} diff --git a/SW.Bitween.Api/Services/Metrics/MetricsRecorder.cs b/SW.Bitween.Api/Services/Metrics/MetricsRecorder.cs new file mode 100644 index 00000000..3966344b --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/MetricsRecorder.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.Services.Cluster; + +namespace SW.Bitween.Services.Metrics; + +public enum ExchangeOutcome { Succeeded, BadResponse, Failed } + +/// Where in its run a failed exchange failed. +public enum FailedAt { Elsewhere, Mapper, Handler } + +public enum AdapterFailure { None, Error, Timeout, LimitStop, Crash } + +/// +/// Counts what this node does, in memory, as it does it: exchanges as they finish, adapter calls as +/// they return, gateway refusals as they are made. Each is a dictionary lookup and a few additions; +/// takes the counts once a minute and adds them to the hourly rows. +/// +/// +/// Counting takes a shared lock and draining an exclusive one, so a count made while a flush swaps +/// the tables lands in one or the other and is never lost. If a node stops without its last flush +/// (killed, not stopped), the last minute of its counts is lost: metrics, not records. +/// +public sealed class MetricsRecorder +{ + /// This node, as its rows are written: the heartbeat's name, cut to the column. + public static string Node { get; } = NodeHeartbeat.NodeName.Length <= 100 ? NodeHeartbeat.NodeName : NodeHeartbeat.NodeName[..100]; + + readonly ReaderWriterLockSlim gate = new(); + Tables current = new(); + + sealed class Tables + { + public readonly ConcurrentDictionary<(DateTime, int, int), ExchangeStats> Exchanges = new(); + public readonly ConcurrentDictionary<(DateTime, string, string, string, string), AdapterStats> Adapters = new(); + public readonly ConcurrentDictionary<(DateTime, string, string, string), ErrorStats> Errors = new(); + } + + /// What a flush takes away: copies, owned by the caller. + public sealed record Snapshot(List Exchanges, List Adapters, List Errors) + { + public bool IsEmpty => Exchanges.Count == 0 && Adapters.Count == 0 && Errors.Count == 0; + } + + public static DateTime HourOf(DateTime utc) => new(utc.Year, utc.Month, utc.Day, utc.Hour, 0, 0, DateTimeKind.Utc); + + public void Exchange(DateTime startedOn, int? subscriptionId, int? partnerId, ExchangeOutcome outcome, + FailedAt failedAt, bool retry, bool manualRetry, long durationMs, long bytesIn, long bytesOut, string error) + { + var hour = HourOf(startedOn); + var subscription = subscriptionId ?? ExchangeStats.None; + gate.EnterReadLock(); + try + { + var row = current.Exchanges.GetOrAdd((hour, subscription, partnerId ?? ExchangeStats.None), + k => new ExchangeStats { Hour = k.Item1, Node = Node, SubscriptionId = k.Item2, PartnerId = k.Item3 }); + lock (row) + { + row.Total++; + switch (outcome) + { + case ExchangeOutcome.Succeeded: row.Succeeded++; break; + case ExchangeOutcome.BadResponse: row.BadResponse++; break; + default: + row.Failed++; + if (failedAt == FailedAt.Mapper) row.FailedAtMapper++; + else if (failedAt == FailedAt.Handler) row.FailedAtHandler++; + break; + } + if (retry) row.Retries++; + if (manualRetry) row.ManualRetries++; + row.BytesIn += Math.Max(0, bytesIn); + row.BytesOut += Math.Max(0, bytesOut); + row.AddDuration(Math.Max(0, durationMs)); + } + if (outcome == ExchangeOutcome.Failed && subscriptionId != null) + CountError(ErrorStats.SubscriptionScope, subscription.ToString(), error); + } + finally + { + gate.ExitReadLock(); + } + } + + public void AdapterCall(string adapterId, string version, string role, string runtime, long durationMs, + AdapterFailure failure, string error) + { + if (string.IsNullOrWhiteSpace(adapterId)) return; + // Lower case throughout: MySQL compares keys without regard to case, and two spellings of + // one id would otherwise be two rows here and one clashing row there. + adapterId = Cut(adapterId.ToLowerInvariant(), 200); + var hour = HourOf(DateTime.UtcNow); + gate.EnterReadLock(); + try + { + var row = current.Adapters.GetOrAdd((hour, adapterId, Cut((version ?? "").ToLowerInvariant(), 50), role, runtime), + k => new AdapterStats { Hour = k.Item1, Node = Node, AdapterId = k.Item2, Version = k.Item3, Role = k.Item4, Runtime = k.Item5 }); + lock (row) + { + row.Calls++; + if (failure != AdapterFailure.None) row.Failures++; + if (failure == AdapterFailure.Timeout) row.Timeouts++; + if (failure == AdapterFailure.LimitStop) row.LimitStops++; + if (failure == AdapterFailure.Crash) row.Crashes++; + row.AddDuration(Math.Max(0, durationMs)); + } + if (failure != AdapterFailure.None) CountError(ErrorStats.AdapterScope, adapterId, error); + } + finally + { + gate.ExitReadLock(); + } + } + + public void Refusal(string gateway, string reason) + { + gate.EnterReadLock(); + try + { + CountError(ErrorStats.GatewayScope, Cut(string.IsNullOrWhiteSpace(gateway) ? "(unknown)" : gateway.ToLowerInvariant(), 200), reason); + } + finally + { + gate.ExitReadLock(); + } + } + + // Called with the shared lock held. + void CountError(string scope, string key, string message) + { + var fingerprint = ErrorFingerprint.Of(message); + var row = current.Errors.GetOrAdd((HourOf(DateTime.UtcNow), scope, key, fingerprint), + k => new ErrorStats { Hour = k.Item1, Node = Node, Scope = k.Item2, ScopeKey = k.Item3, Fingerprint = k.Item4 }); + lock (row) + { + row.Count++; + row.Sample = ErrorFingerprint.Sample(message); + row.LastSeenOn = DateTime.UtcNow; + } + } + + /// Takes everything counted so far, leaving empty tables to count into. + public Snapshot Drain() + { + Tables taken; + gate.EnterWriteLock(); + try + { + taken = current; + current = new Tables(); + } + finally + { + gate.ExitWriteLock(); + } + return new Snapshot(taken.Exchanges.Values.ToList(), taken.Adapters.Values.ToList(), taken.Errors.Values.ToList()); + } + + /// + /// Puts back counts a flush couldn't write, to go with the next one. Older than two days, they + /// are let go: a database that has been unreachable that long has bigger news than a chart. + /// + public void Restore(Snapshot snapshot) + { + var keep = HourOf(DateTime.UtcNow).AddDays(-2); + gate.EnterReadLock(); + try + { + foreach (var s in snapshot.Exchanges.Where(s => s.Hour >= keep)) + { + var row = current.Exchanges.GetOrAdd((s.Hour, s.SubscriptionId, s.PartnerId), + k => new ExchangeStats { Hour = k.Item1, Node = Node, SubscriptionId = k.Item2, PartnerId = k.Item3 }); + lock (row) row.Add(s); + } + foreach (var s in snapshot.Adapters.Where(s => s.Hour >= keep)) + { + var row = current.Adapters.GetOrAdd((s.Hour, s.AdapterId, s.Version, s.Role, s.Runtime), + k => new AdapterStats { Hour = k.Item1, Node = Node, AdapterId = k.Item2, Version = k.Item3, Role = k.Item4, Runtime = k.Item5 }); + lock (row) row.Add(s); + } + foreach (var s in snapshot.Errors.Where(s => s.Hour >= keep)) + { + var row = current.Errors.GetOrAdd((s.Hour, s.Scope, s.ScopeKey, s.Fingerprint), + k => new ErrorStats { Hour = k.Item1, Node = Node, Scope = k.Item2, ScopeKey = k.Item3, Fingerprint = k.Item4 }); + lock (row) + { + row.Count += s.Count; + row.Sample ??= s.Sample; + if (s.LastSeenOn > row.LastSeenOn) row.LastSeenOn = s.LastSeenOn; + } + } + } + finally + { + gate.ExitReadLock(); + } + } + + static string Cut(string value, int length) => value.Length <= length ? value : value[..length]; +} diff --git a/SW.Bitween.Api/Services/Metrics/QueueSnapshotJob.cs b/SW.Bitween.Api/Services/Metrics/QueueSnapshotJob.cs new file mode 100644 index 00000000..664b5d4f --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/QueueSnapshotJob.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain.Metrics; +using SW.Bus.RabbitMqExtensions; +using SW.Scheduler; + +namespace SW.Bitween.Services.Metrics; + +/// +/// Records the broker's queue totals every few minutes, for the backlog trend. One call to the +/// RabbitMQ management API and one small row; nothing at all when the management API isn't set up. +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class QueueSnapshotJob(BitweenDbContext db, BitweenOptions options, IServiceProvider services) : IScheduledJob +{ + public static readonly TimeSpan Every = TimeSpan.FromMinutes(5); + + public async Task Execute() + { + if (string.IsNullOrWhiteSpace(options.RabbitMqManagementUrl)) return; + var dashboard = services.GetService(); + if (dashboard == null) return; + + var summary = await dashboard.GetSummaryAsync(); + var now = DateTime.UtcNow; + // Cut to the interval: if more than one node samples at once, the key keeps one of them. + var at = new DateTime(now.Ticks - now.Ticks % Every.Ticks, DateTimeKind.Utc); + if (await db.Set().AnyAsync(s => s.At == at)) return; + db.Add(new QueueSnapshot + { + At = at, + QueueDepth = summary.TotalQueueDepth, + RetryBacklog = summary.TotalRetryBacklog, + DeadLetterBacklog = summary.TotalDeadLetterBacklog, + IncomingRate = summary.TotalIncomingRate, + AckRate = summary.TotalAckRate, + Consumers = summary.TotalConsumers, + UnhealthyConsumers = summary.UnhealthyConsumers, + }); + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateException) + { + // Another node wrote this interval's snapshot first. One is all that's wanted. + } + } +} diff --git a/SW.Bitween.Api/Services/Metrics/SubscriptionHealth.cs b/SW.Bitween.Api/Services/Metrics/SubscriptionHealth.cs new file mode 100644 index 00000000..7c0467f3 --- /dev/null +++ b/SW.Bitween.Api/Services/Metrics/SubscriptionHealth.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Metrics; + +namespace SW.Bitween.Services.Metrics; + +/// One subscription's health over a window, as the lists and the dashboard show it. +public class SubscriptionHealthRow +{ + public int SubscriptionId { get; set; } + public string Name { get; set; } + public string Type { get; set; } + /// healthy, degraded, failing, silent, idle, paused, inactive. + public string Status { get; set; } + public ExchangeTotals Totals { get; set; } + public double? SuccessRate => Totals.SuccessRate; + public long? P95Ms => Totals.P95Ms; + /// The hour of its latest exchange in the last 30 days. + public DateTime? LastExchangeHour { get; set; } + /// For a silent one: how long it has been quiet, and what its usual gap is. + public double? QuietHours { get; set; } + public double? UsualGapHours { get; set; } + /// Exchanges per day over the window, oldest first. + public long[] Daily { get; set; } +} + +/// +/// Whether each subscription is doing what it usually does. Most of it is plain rates; the part +/// that can't be had elsewhere is "silent": a subscription that normally has traffic and has gone +/// quiet, which nothing fails on and so nothing reported. Usual is read from the last week: a +/// subscription with traffic in many of its hours is expected again soon, one with traffic once a +/// day is not missed for a day. +/// +public static class SubscriptionHealth +{ + /// Hours with traffic in the last week before a subscription is regular enough to be missed. + public const int RegularAfterActiveHours = 6; + public const double SilentAfterAtLeastHours = 6; + public const double SilentAfterGaps = 3; + + public static async Task> ComputeAsync(BitweenDbContext db, int days, DateTime now) + { + var to = MetricsRecorder.HourOf(now).AddHours(1); + var from = to.AddDays(-days); + var week = to.AddDays(-7); + var month = to.AddDays(-30); + + var subscriptions = await db.Set().AsNoTracking() + .Select(s => new { s.Id, s.Name, s.Type, s.Inactive, s.PausedOn }) + .ToListAsync(); + + var totals = (await MetricsQueries.Exchanges(db, from, to).GroupBy(s => s.SubscriptionId).SumBy()) + .ToDictionary(t => t.Key); + var activeHours = await MetricsQueries.Exchanges(db, week, to) + .Where(s => s.Total > 0) + .GroupBy(s => s.SubscriptionId) + .Select(g => new { g.Key, Hours = g.Select(x => x.Hour).Distinct().Count() }) + .ToDictionaryAsync(x => x.Key, x => x.Hours); + var last = await MetricsQueries.Exchanges(db, month, to) + .Where(s => s.Total > 0) + .GroupBy(s => s.SubscriptionId) + .Select(g => new { g.Key, Last = g.Max(x => x.Hour) }) + .ToDictionaryAsync(x => x.Key, x => MetricsQueries.Utc(x.Last)); + var dailyRows = await MetricsQueries.Exchanges(db, from, to) + .GroupBy(s => new { s.SubscriptionId, Day = s.Hour.Date }) + .Select(g => new { g.Key.SubscriptionId, g.Key.Day, Total = g.Sum(x => (long)x.Total) }) + .ToListAsync(); + var firstDay = from.Date; + var daily = dailyRows.GroupBy(r => r.SubscriptionId).ToDictionary(g => g.Key, g => + { + var series = new long[days]; + foreach (var r in g) + { + var i = (int)(r.Day.Date - firstDay).TotalDays; + if (i >= 0 && i < days) series[i] += r.Total; + } + return series; + }); + + return subscriptions.Select(s => + { + var t = totals.TryGetValue(s.Id, out var found) ? found : new ExchangeTotals { Key = s.Id }; + var row = new SubscriptionHealthRow + { + SubscriptionId = s.Id, + Name = s.Name, + Type = s.Type.ToString(), + Totals = t, + LastExchangeHour = last.TryGetValue(s.Id, out var l) ? l : null, + Daily = daily.TryGetValue(s.Id, out var d) ? d : new long[days], + }; + row.Status = Judge(row, s.Inactive, s.PausedOn != null, activeHours.GetValueOrDefault(s.Id), now); + return row; + }).ToList(); + } + + static string Judge(SubscriptionHealthRow row, bool inactive, bool paused, int activeHoursLastWeek, DateTime now) + { + if (inactive) return "inactive"; + if (paused) return "paused"; + + if (row.LastExchangeHour != null && activeHoursLastWeek >= RegularAfterActiveHours) + { + var quiet = (now - row.LastExchangeHour.Value.AddHours(1)).TotalHours; + var usualGap = 168.0 / activeHoursLastWeek; + if (quiet > Math.Max(SilentAfterAtLeastHours, SilentAfterGaps * usualGap)) + { + row.QuietHours = Math.Round(quiet, 1); + row.UsualGapHours = Math.Round(usualGap, 1); + return "silent"; + } + } + + if (row.Totals.Total == 0) return "idle"; + var rate = row.Totals.SuccessRate ?? 100; + return rate < 90 ? "failing" : rate < 99 ? "degraded" : "healthy"; + } +} diff --git a/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs index 0aad6492..8517419b 100644 --- a/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs +++ b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs @@ -17,6 +17,9 @@ public class NativeAdapterDiscoveryService( BitweenOptions bitweenOptions) { public const string NativePrefix = "native"; + + public static bool IsNative(string adapterId) => + adapterId != null && adapterId.StartsWith(NativePrefix, StringComparison.OrdinalIgnoreCase); public Dictionary GetStartupValues(string adapterId) { var result = new Dictionary(); @@ -69,7 +72,7 @@ public Dictionary GetExpectedStartupValues(string adapterId) else { // A secret's default is withheld here as it is everywhere else an adapter is - // described to a client — see AdapterStartupValues.WithoutSecretDefaults. + // described to a client - see AdapterStartupValues.WithoutSecretDefaults. var shown = prop.GetCustomAttribute() != null ? null : defaultValue; result[prop.Name] = $"{prop.Name} ({shown ?? "null"})"; } @@ -105,7 +108,7 @@ public INativeInfolinkMapper GetNativeMapper(string adapterId, Dictionary /// Whether the mapper with this id takes its partner and global values as context instead of - /// out of the payload — see . + /// out of the payload - see . /// /// /// Deliberately does not go through : that initializes the diff --git a/SW.Bitween.Api/Services/NotifierDelivery.cs b/SW.Bitween.Api/Services/NotifierDelivery.cs new file mode 100644 index 00000000..40983f77 --- /dev/null +++ b/SW.Bitween.Api/Services/NotifierDelivery.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Hands a notifier's handler one . Shared by exchange results +/// and the alerts that have no exchange, so all reach a notifier the same way, quick retries included. +/// +public static class NotifierDelivery +{ + /// + /// Null once delivered; otherwise what the last attempt threw. fills + /// the handler's xchangeid property; an alert without an exchange passes null and names the + /// file after its own id instead. + /// + public static async Task SendAsync(IAdapterInvoker adapterInvoker, Notifier notifier, + XchangeResultNotification notification, string xchangeId, string correlationId) + { + var handlerProperties = notifier.HandlerProperties.ToDictionary(); + if (xchangeId != null) handlerProperties["xchangeid"] = xchangeId; + + // A couple of quick retries for a mail server that hiccups; a send that failed used to be + // recorded and never tried again. + Exception lastError = null; + for (var attempt = 0; attempt < 3; attempt++) + { + if (attempt > 0) await Task.Delay(TimeSpan.FromSeconds(attempt * 2)); + try + { + await adapterInvoker.InvokeAsync( + notifier.HandlerId, AdapterRole.Handler, nameof(IInfolinkHandler.Handle), + new XchangeFile(JsonConvert.SerializeObject(notification), xchangeId ?? notification.Id), + handlerProperties, correlationId); + return null; + } + catch (Exception ex) + { + lastError = ex; + } + } + + return lastError; + } +} diff --git a/SW.Bitween.Api/Services/OutboxDispatcher.cs b/SW.Bitween.Api/Services/OutboxDispatcher.cs new file mode 100644 index 00000000..2b7bb0c2 --- /dev/null +++ b/SW.Bitween.Api/Services/OutboxDispatcher.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; + +namespace SW.Bitween; + +/// +/// Publishes the outbox rows that were not published straight after their commit - because the +/// broker was down, or because they were saved inside a caller's transaction - and clears out old +/// published ones. +/// +/// +/// Every replica runs one. A row is claimed with a conditional update before it is published, so +/// two replicas never publish the same row at the same time; a claim expires, so a replica that +/// dies holding one does not strand it. +/// +public class OutboxDispatcher(IServiceScopeFactory scopes, ILogger logger) : BackgroundService +{ + private static readonly TimeSpan PollEvery = TimeSpan.FromSeconds(5); + + // Left alone this long, so the publish straight after the commit gets its chance first. + private static readonly TimeSpan Grace = TimeSpan.FromSeconds(10); + + private static readonly TimeSpan ClaimFor = TimeSpan.FromMinutes(1); + private static readonly TimeSpan KeepPublished = TimeSpan.FromDays(1); + private const int BatchSize = 100; + + private DateTime _nextCleanup = DateTime.MinValue; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + var published = await DispatchOnceAsync(stoppingToken); + if (published > 0) + logger.LogInformation("Published {Count} message(s) left in the outbox.", published); + + if (DateTime.UtcNow >= _nextCleanup) + { + await CleanUpAsync(stoppingToken); + _nextCleanup = DateTime.UtcNow.AddHours(1); + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (Exception ex) + { + logger.LogError(ex, "The outbox dispatcher failed a pass; it will try again."); + } + + try { await Task.Delay(PollEvery, stoppingToken); } + catch (OperationCanceledException) { return; } + } + } + + /// How many were published. + public async Task DispatchOnceAsync(CancellationToken cancellationToken) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + var cutoff = now - Grace; + var candidates = await db.Set().AsNoTracking() + .Where(m => m.PublishedOn == null && m.CreatedOn < cutoff && + (m.ClaimedUntil == null || m.ClaimedUntil < now)) + .OrderBy(m => m.Id) + .Take(BatchSize) + .ToListAsync(cancellationToken); + + var claimed = new System.Collections.Generic.List(); + foreach (var message in candidates) + { + var until = DateTime.UtcNow + ClaimFor; + var claimTime = DateTime.UtcNow; + var won = await db.Set() + .Where(m => m.Id == message.Id && m.PublishedOn == null && + (m.ClaimedUntil == null || m.ClaimedUntil < claimTime)) + .ExecuteUpdateAsync(u => u.SetProperty(m => m.ClaimedUntil, until), cancellationToken); + if (won == 1) claimed.Add(message); + } + + if (claimed.Count == 0) return 0; + + var published = await db.PublishOutboxAsync(claimed, cancellationToken); + if (published < claimed.Count) + logger.LogWarning("{Count} outbox message(s) still could not be published; retrying shortly.", + claimed.Count - published); + + return published; + } + + private async Task CleanUpAsync(CancellationToken cancellationToken) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var before = DateTime.UtcNow - KeepPublished; + + int deleted; + do + { + var batch = await db.Set() + .Where(m => m.PublishedOn != null && m.PublishedOn < before) + .OrderBy(m => m.Id) + .Select(m => m.Id) + .Take(1000) + .ToListAsync(cancellationToken); + deleted = batch.Count == 0 + ? 0 + : await db.Set().Where(m => batch.Contains(m.Id)).ExecuteDeleteAsync(cancellationToken); + } while (deleted > 0 && !cancellationToken.IsCancellationRequested); + } +} diff --git a/SW.Bitween.Api/Services/PartnerKeyHashing.cs b/SW.Bitween.Api/Services/PartnerKeyHashing.cs new file mode 100644 index 00000000..5faf10b3 --- /dev/null +++ b/SW.Bitween.Api/Services/PartnerKeyHashing.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; + +namespace SW.Bitween.Services; + +/// +/// Converts partner API keys stored before keys were hashed. Runs at startup after the SYSTEM +/// partner's published key has been dealt with; once every key is hashed it finds nothing to do. +/// +public static class PartnerKeyHashing +{ + public static IHost HashStoredPartnerKeys(this IHost host) + { + using var scope = host.Services.CreateScope(); + HashStoredKeys( + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService>()) + .GetAwaiter().GetResult(); + return host; + } + + public static async Task HashStoredKeys(BitweenDbContext dbContext, ILogger logger) + { + var partners = await dbContext.Set() + .Where(p => p.ApiCredentials.Any(c => !c.Key.StartsWith(PartnerKeyHash.Prefix))) + .ToListAsync(); + if (partners.Count == 0) return; + + // All or nothing: a partner left with some keys removed and not yet re-added would turn + // away the callers that use them. + await using var transaction = dbContext.Database.CurrentTransaction is null + ? await dbContext.Database.BeginTransactionAsync() + : null; + + var converted = 0; + foreach (var partner in partners) + { + var hashed = partner.ApiCredentials.Where(c => PartnerKeyHash.IsHashed(c.Key)).ToList(); + var plain = partner.ApiCredentials.Where(c => !PartnerKeyHash.IsHashed(c.Key)).ToList(); + + // Credentials compare by name, so the plain ones go before their hashed copies come in. + partner.SetApiCredentials(hashed); + await dbContext.SaveChangesAsync(); + partner.SetApiCredentials(hashed.Concat(plain.Select(c => new ApiCredential(c.Name, c.Key)))); + await dbContext.SaveChangesAsync(); + converted += plain.Count; + } + + if (transaction is not null) await transaction.CommitAsync(); + logger.LogInformation("Stored {Count} partner API key(s) as hashes.", converted); + } +} diff --git a/SW.Bitween.Api/Services/PrivateAzureContainer.cs b/SW.Bitween.Api/Services/PrivateAzureContainer.cs index 8d79d633..049c5715 100644 --- a/SW.Bitween.Api/Services/PrivateAzureContainer.cs +++ b/SW.Bitween.Api/Services/PrivateAzureContainer.cs @@ -10,8 +10,8 @@ namespace SW.Bitween; /// -/// Keeps Bitween's container private on Azure. Azure has no private setting per file — the container -/// decides for everything in it — and the storage library creates the container open to everyone, so at +/// Keeps Bitween's container private on Azure. Azure has no private setting per file - the container +/// decides for everything in it - and the storage library creates the container open to everyone, so at /// startup it's switched to private. Bitween reads and writes with its own credentials and hands out links /// it serves itself, so nothing of Bitween's needs anonymous access. When the switch isn't allowed, the /// reason goes to for the settings page. diff --git a/SW.Bitween.Api/Services/QuietSubscriptionsJob.cs b/SW.Bitween.Api/Services/QuietSubscriptionsJob.cs new file mode 100644 index 00000000..7b45ad13 --- /dev/null +++ b/SW.Bitween.Api/Services/QuietSubscriptionsJob.cs @@ -0,0 +1,178 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Bitween.Services.Adapters; +using SW.Scheduler; + +namespace SW.Bitween; + +/// +/// Finds subscriptions whose went unmet - a partner that stops sending +/// fails nothing, so nothing else would say so - and tells the notifiers that watch them for failed +/// results. Once when it goes quiet, once when exchanges arrive again. +/// +/// Every question asked of the exchanges is bounded in time: has one started since a moment no +/// further back than the expected gap (or an hour, for the way back). Both the subscription and the +/// start time are indexed, so neither depends on how much history the instance keeps. +/// +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class QuietSubscriptionsJob(BitweenDbContext db, IAdapterInvoker adapterInvoker, ILogger logger) + : IScheduledJob +{ + public const string GoneQuiet = "gone-quiet"; + public const string Back = "back"; + + // How far back an alert looks for the last exchange, to say when traffic stopped. + static readonly TimeSpan LastSeenLookback = TimeSpan.FromDays(7); + + // Exchanges since QuietSince are looked for in at least this much of the recent past. A job that + // runs every few minutes sees each one long before it falls out; one older than this followed by + // silence means the subscription is quiet again anyway, so it stays flagged. + static readonly TimeSpan BackLookback = TimeSpan.FromHours(1); + + public Task Execute() => Run(DateTime.UtcNow); + + /// One pass as at . + public async Task Run(DateTime utcNow) + { + // Few rows and narrow, so filtered here rather than by comparing a JSON column in SQL. + var subscriptions = (await db.Set().AsNoTracking() + .Select(s => new { s.Id, s.Name, s.DocumentId, s.Inactive, s.PausedOn, s.ExpectTraffic, s.QuietSince }) + .ToListAsync()) + .Where(s => s.QuietSince != null || (s.ExpectTraffic != null && !s.Inactive && s.PausedOn == null)) + .ToList(); + + foreach (var s in subscriptions) + { + try + { + TrafficWindow window = null; + if (s.ExpectTraffic != null) + { + try + { + window = TrafficWindow.Parse(s.ExpectTraffic); + } + catch (SW.PrimitiveTypes.SWValidationException e) + { + // Saved valid, so most likely a time zone this host lacks. + logger.LogWarning("Subscription {SubscriptionId} expects traffic in a way that can't be checked here: {Reason}", + s.Id, e.Message); + } + } + + if (s.QuietSince is { } quietSince) + { + var lookback = window?.Every > BackLookback ? window.Every : BackLookback; + var since = quietSince > utcNow - lookback ? quietSince : utcNow - lookback; + var first = await db.Set().AsNoTracking() + .Where(x => x.SubscriptionId == s.Id && x.StartedOn > since) + .MinAsync(x => (DateTime?)x.StartedOn); + // Cleared outside the window too: traffic is back whenever it comes back. + if (first != null && await SetQuietSince(s.Id, quietSince, null)) + { + var stoppedAt = await LastStartedBefore(s.Id, quietSince) ?? quietSince; + await Notify(s.Id, s.Name, s.DocumentId, Back, + $"Exchanges arriving again after {Duration(first.Value - stoppedAt)}.", utcNow); + } + continue; + } + + if (window?.QuietUnlessAfter(utcNow) is not { } cutoff) continue; + var arrived = await db.Set().AsNoTracking() + .AnyAsync(x => x.SubscriptionId == s.Id && x.StartedOn > cutoff); + if (arrived || !await SetQuietSince(s.Id, null, utcNow)) continue; + + var last = await LastStartedBefore(s.Id, utcNow); + var silence = last == null + ? $"No exchanges in the last {LastSeenLookback.TotalDays:0} days" + : $"No exchanges since {last.Value:yyyy-MM-dd HH:mm} UTC"; + await Notify(s.Id, s.Name, s.DocumentId, GoneQuiet, + $"{silence}; {s.Name} expects at least one {window.Describe()}.", utcNow); + } + catch (Exception e) + { + // One subscription's trouble mustn't keep the rest from being checked. + logger.LogError(e, "Checking subscription {SubscriptionId} for missing traffic failed.", s.Id); + } + } + } + + /// + /// Moves QuietSince from to , and says whether + /// this pass did it. Conditional, so an alert goes out once even if two passes overlap. + /// + async Task SetQuietSince(int subscriptionId, DateTime? expected, DateTime? value) => + await db.Set() + .Where(x => x.Id == subscriptionId && x.QuietSince == expected) + .ExecuteUpdateAsync(u => u.SetProperty(x => x.QuietSince, value)) == 1; + + Task LastStartedBefore(int subscriptionId, DateTime at) + { + var from = at - LastSeenLookback; + return db.Set().AsNoTracking() + .Where(x => x.SubscriptionId == subscriptionId && x.StartedOn <= at && x.StartedOn > from) + .MaxAsync(x => (DateTime?)x.StartedOn); + } + + /// + /// The notifiers watching the subscription for failed results get the same payload an exchange + /// result sends. No XchangeNotification row: there's no exchange for it to belong to. + /// + async Task Notify(int subscriptionId, string subscriptionName, int documentId, string kind, string sentence, DateTime utcNow) + { + logger.LogWarning("Subscription {SubscriptionId} ({SubscriptionName}) {Event}: {Detail}", + subscriptionId, subscriptionName, kind, sentence); + + var notifiers = (await db.Set().AsNoTracking() + .Where(n => !n.Inactive && n.RunOnFailedResult && n.HandlerId != null) + .ToListAsync()) + .Where(n => n.RunOnSubscriptions?.Contains(subscriptionId) == true) + .ToList(); + if (notifiers.Count == 0) return; + + var documentName = await db.Set().AsNoTracking() + .Where(d => d.Id == documentId).Select(d => d.Name).FirstOrDefaultAsync(); + // Stands in for the exchange id, so a handler that files or threads by it has one. + var alertId = $"{kind}-{subscriptionId}-{utcNow:yyyyMMddHHmmss}"; + var notification = new XchangeResultNotification + { + Id = alertId, + Success = kind == Back, + Exception = sentence, + StartedOn = utcNow, + FinishedOn = utcNow, + SubscriptionId = subscriptionId, + SubscriptionName = subscriptionName, + DocumentId = documentId, + DocumentName = documentName, + CorrelationId = alertId, + Event = kind, + }; + + foreach (var notifier in notifiers) + { + var error = await NotifierDelivery.SendAsync(adapterInvoker, notifier, notification, null, alertId); + if (error == null) + logger.LogInformation("Told notifier {NotifierId} ({NotifierName}) subscription {SubscriptionId} {Event}.", + notifier.Id, notifier.Name, subscriptionId, kind); + else + logger.LogError(error, "Notifier {NotifierId} ({NotifierName}) could not be told subscription {SubscriptionId} {Event}.", + notifier.Id, notifier.Name, subscriptionId, kind); + } + } + + static string Duration(TimeSpan span) + { + var minutes = (int)Math.Max(1, Math.Round(span.TotalMinutes)); + if (minutes < 120) return minutes == 1 ? "1 minute" : $"{minutes} minutes"; + var hours = (int)Math.Round(span.TotalHours); + if (hours < 48) return $"{hours} hours"; + return $"{(int)Math.Round(span.TotalDays)} days"; + } +} diff --git a/SW.Bitween.Api/Services/ReceivingJob.cs b/SW.Bitween.Api/Services/ReceivingJob.cs index d92dd6c4..5fb43602 100644 --- a/SW.Bitween.Api/Services/ReceivingJob.cs +++ b/SW.Bitween.Api/Services/ReceivingJob.cs @@ -40,7 +40,7 @@ public async Task Execute(ReceivingJobParams jobParams) // Advances regardless of outcome: the Quartz trigger fires on its own cron no // matter what happens below, so "next run" has to track that, not the receive - // step's success — otherwise a receiver that keeps failing freezes ReceiveOn + // step's success - otherwise a receiver that keeps failing freezes ReceiveOn // in the past forever while the job keeps firing on schedule underneath it. // Isolated in its own try: a schedule problem is unrelated to receiving and // must not stop the step below from running. @@ -61,7 +61,7 @@ public async Task Execute(ReceivingJobParams jobParams) // The receiver's cursor is namespaced by this. Without it, two subscriptions // polling one data source share a cursor and split the rows between them. .WithSubscription(rec.Id); - await RunReceiver(rec.ReceiverId, startupParameters, rec.Id, createdExchangeIds); + await RunReceiver(rec.ReceiverRef, startupParameters, rec.Id, createdExchangeIds); rec.SetHealth(); RecordAttempt(rec.Id, startedOn, createdExchangeIds.Count > 0 ? ReceiveOutcome.Received : ReceiveOutcome.NoNewData, @@ -108,18 +108,54 @@ private async Task RunReceiver( serverlessId, AdapterRole.Receiver, startupParameters); await session.InvokeAsync(nameof(IInfolinkReceiver.Initialize)); - var fileList = (await session.InvokeAsync>(nameof(IInfolinkReceiver.ListFiles))).ToList(); - logger.LogInformation("Subscription '{SubId}' found {Count} items for retrieval.", subId, fileList.Count); + // Finalize runs whatever happens in between: it is what closes the connection, and for POP3 + // what commits the deletes of the messages that were taken. + var failed = new List(); + try + { + var fileList = (await session.InvokeAsync>(nameof(IInfolinkReceiver.ListFiles))).ToList(); + + logger.LogInformation("Subscription '{SubId}' found {Count} items for retrieval.", subId, fileList.Count); + + // One file at a time, each on its own. A file that could not be read or submitted used to + // end the run, and since it stays at the source it ended every run after: one corrupt + // file at the top of the listing and nothing behind it was ever received. Now it is left + // where it is, reported, and the rest go through. + foreach (var file in fileList) + { + string exchangeId; + try + { + var xchangeFile = await session.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); + logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); + exchangeId = await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + } + catch (Exception ex) + { + logger.LogError(ex, "Subscription {SubId} could not receive {File}; it is left at the source.", subId, file); + failed.Add($"{file}: {ex.Message}"); + continue; + } - foreach (var file in fileList) + createdExchangeIds.Add(exchangeId); + await session.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); + } + } + finally { - var xchangeFile = await session.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); - logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); - createdExchangeIds.Add(await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile)); - await session.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); + await session.InvokeAsync(nameof(IInfolinkReceiver.Finalize)); } - await session.InvokeAsync(nameof(IInfolinkReceiver.Finalize)); + if (failed.Count > 0) + throw new ReceiverPartlyFailedException(failed); } } + +/// +/// Some items of a run could not be received. Thrown once the rest have been, so the run is +/// recorded as failed with each item's reason while the exchanges it did create are kept. +/// +public class ReceiverPartlyFailedException(IReadOnlyCollection failures) + : Exception($"{failures.Count} item(s) could not be received and were left at the source:\n" + + string.Join("\n", failures)); diff --git a/SW.Bitween.Api/Services/ResultWait.cs b/SW.Bitween.Api/Services/ResultWait.cs new file mode 100644 index 00000000..ba477b92 --- /dev/null +++ b/SW.Bitween.Api/Services/ResultWait.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace SW.Bitween; + +/// +/// Holds a synchronous gateway call open while its exchange is processed, polling for the result. +/// +/// +/// The loops this replaced compared the polling step - capped at eight seconds - with the requested +/// wait, so any wait of eight seconds or more, the default of 120 included, never ended. An exchange +/// that never got a result held its request, a DI scope and a database context for as long as the +/// process lived, and a caller could open as many as it liked. Now the wait is measured in elapsed +/// time, capped, and ends when the caller goes away. +/// +public static class ResultWait +{ + public const int DefaultSeconds = 120; + + /// The wait a caller asked for, or the default, never above the configured ceiling. + public static int Clamp(int? requested, int maxSeconds) + { + var seconds = requested is null or <= 0 ? DefaultSeconds : requested.Value; + return Math.Min(seconds, Math.Max(1, maxSeconds)); + } + + /// True once says so; false when the time is up or the caller left. + public static async Task UntilAsync(Func> isAvailable, int seconds, CancellationToken cancellationToken) + { + var deadline = TimeSpan.FromSeconds(seconds); + var clock = Stopwatch.StartNew(); + int step = 1, previous = 1; + + while (clock.Elapsed < deadline) + { + var remaining = deadline - clock.Elapsed; + var delay = TimeSpan.FromSeconds(step) < remaining ? TimeSpan.FromSeconds(step) : remaining; + try + { + await Task.Delay(delay, cancellationToken); + } + catch (OperationCanceledException) + { + return false; + } + + if (await isAvailable()) return true; + + (step, previous) = (Math.Min(step + previous, 8), step); + } + + return false; + } +} diff --git a/SW.Bitween.Api/Services/Retention/ExchangeArchive.cs b/SW.Bitween.Api/Services/Retention/ExchangeArchive.cs index ee6d9cdf..cf3fde79 100644 --- a/SW.Bitween.Api/Services/Retention/ExchangeArchive.cs +++ b/SW.Bitween.Api/Services/Retention/ExchangeArchive.cs @@ -23,8 +23,8 @@ namespace SW.Bitween; /// /// Files go to {archive prefix}/{subscription id}/yyyy/MM/dd/{first promoted value}_{exchange id}.json, /// dated by when the exchange started. The first promoted property of the exchange's information type -/// leads the name so an exchange can be found by it; the id keeps two with the same value — the -/// attempts of one retry chain — from overwriting each other. Rewriting the same exchange produces the +/// leads the name so an exchange can be found by it; the id keeps two with the same value - the +/// attempts of one retry chain - from overwriting each other. Rewriting the same exchange produces the /// same key, so a run that stopped part-way can simply run again. /// /// @@ -54,7 +54,7 @@ public class ExchangeArchive(BitweenDbContext dbContext, XchangeService xchangeS public string Prefix => PrefixFor(options.DocumentPrefix); /// - /// Out of the temp folders on purpose — the bucket's rule for temp30/ would otherwise delete + /// Out of the temp folders on purpose - the bucket's rule for temp30/ would otherwise delete /// the archive along with the files it keeps. The rest of the prefix stays, so deployments that share /// a bucket under different prefixes keep separate archives. /// diff --git a/SW.Bitween.Api/Services/Retention/ExchangeRetentionJob.cs b/SW.Bitween.Api/Services/Retention/ExchangeRetentionJob.cs index 115089c8..d787ffa4 100644 --- a/SW.Bitween.Api/Services/Retention/ExchangeRetentionJob.cs +++ b/SW.Bitween.Api/Services/Retention/ExchangeRetentionJob.cs @@ -46,7 +46,7 @@ public async Task Execute() } // Something couldn't be archived. It's kept, and so is everything after it, until a later - // run gets it into the archive — retrying here would only fail the same way. + // run gets it into the archive - retrying here would only fail the same way. if (done.Count < batch.Count) { logger.LogError( diff --git a/SW.Bitween.Api/Services/Retention/RetentionPlanner.cs b/SW.Bitween.Api/Services/Retention/RetentionPlanner.cs index ea7f26f2..b4918127 100644 --- a/SW.Bitween.Api/Services/Retention/RetentionPlanner.cs +++ b/SW.Bitween.Api/Services/Retention/RetentionPlanner.cs @@ -11,7 +11,7 @@ namespace SW.Bitween; /// -/// Works out what the retention settings do — as they stand, or as someone is about to change them — so +/// Works out what the retention settings do - as they stand, or as someone is about to change them - so /// the settings page can say it before anything is saved: how long files and exchanges are kept, what /// the next run removes, and which exchanges are left without their files. /// diff --git a/SW.Bitween.Api/Services/RetryAlertResolver.cs b/SW.Bitween.Api/Services/RetryAlertResolver.cs index 3e9a95a7..06b047d6 100644 --- a/SW.Bitween.Api/Services/RetryAlertResolver.cs +++ b/SW.Bitween.Api/Services/RetryAlertResolver.cs @@ -10,7 +10,7 @@ public class RetryAlertTarget public required string HandlerId { get; init; } public IReadOnlyDictionary HandlerProperties { get; init; } - /// Which level won — shown in the UI so a surprising destination can be traced. + /// Which level won - shown in the UI so a surprising destination can be traced. public required RetryAlertLevel Level { get; init; } } @@ -39,7 +39,7 @@ public static class RetryAlertResolver /// The matched group. null when the group no longer exists in the policy. /// /// The named policy, or null when the subscription uses an inline - /// — those have no policy row, so only the group and the + /// - those have no policy row, so only the group and the /// override levels can configure an alert. /// public static RetryAlertTarget Resolve(RetryAlertOverride subscriptionOverride, RetryGroup group, diff --git a/SW.Bitween.Api/Services/RetryAlertService.cs b/SW.Bitween.Api/Services/RetryAlertService.cs index 3721435b..046eb4b6 100644 --- a/SW.Bitween.Api/Services/RetryAlertService.cs +++ b/SW.Bitween.Api/Services/RetryAlertService.cs @@ -18,7 +18,7 @@ namespace SW.Bitween; /// /// Deliberately a separate consumer rather than another branch inside XchangeService's /// result handling: alerts go through a customer-configured adapter that may be slow or broken, and -/// on the shared result queue that would hold up — or fail — the ordinary notifiers for the same +/// on the shared result queue that would hold up - or fail - the ordinary notifiers for the same /// exchange. Its own means its own queue, and the two fail apart. /// public class RetryAlertService( @@ -30,7 +30,7 @@ public async Task Process(RetryBudgetExhaustedEvent message) { // The bus is at-least-once, and the exhaustion is stamped on the exchange rather than on // the send, so a redelivery would otherwise email the same alert twice. A *successful* log - // row is the record that it already went out — matching any alert row would let one failed + // row is the record that it already went out - matching any alert row would let one failed // send stand in for a delivery and silence every later attempt. The name is checked too, so // a row written by some other path can never be mistaken for this alert. var alreadySent = await dbContext.Set() diff --git a/SW.Bitween.Api/Services/RetryGroupBudget.cs b/SW.Bitween.Api/Services/RetryGroupBudget.cs index deb10836..b9593b60 100644 --- a/SW.Bitween.Api/Services/RetryGroupBudget.cs +++ b/SW.Bitween.Api/Services/RetryGroupBudget.cs @@ -29,7 +29,7 @@ public class RetryGroupBudget( /// Because it commits on its own rather than with the caller's SaveChangesAsync, a slot /// can be charged for a retry that is never scheduled if that later save fails. That errs /// toward retrying less than the cap allows, and RetryPolicies/resetusage can return it. - /// The alternative — an explicit transaction spanning the caller's save — would publish this + /// The alternative - an explicit transaction spanning the caller's save - would publish this /// xchange's bus events before the commit, since BitweenDbContext dispatches domain events /// inside SaveChangesAsync. /// @@ -37,7 +37,7 @@ public class RetryGroupBudget( public async Task TryConsume(Guid groupId, int maxAttemptsTotal) { // A group configured to allow no retries at all has no budget to exhaust, so it never - // alerts — otherwise every single failure under it would raise one. + // alerts - otherwise every single failure under it would raise one. if (maxAttemptsTotal <= 0) return RetryBudgetClaim.Denied; if (await TryIncrement(dbContext, groupId, maxAttemptsTotal)) return RetryBudgetClaim.Allowed; @@ -70,7 +70,7 @@ public async Task TryConsume(Guid groupId, int maxAttemptsTota catch (DbUpdateException) { // The winner of the insert race already holds a row, so this is the ordinary - // increment path again — including the case where their row is already full. + // increment path again - including the case where their row is already full. return await TryIncrement(dbContext, groupId, maxAttemptsTotal) ? RetryBudgetClaim.Allowed : await ClaimExhaustionAlert(groupId, maxAttemptsTotal); @@ -84,14 +84,14 @@ public async Task TryConsume(Guid groupId, int maxAttemptsTota /// /// An exhausted total is a statement about a downstream that was failing, and one success says /// that is no longer true. Nothing else can say it: an exhausted group schedules no further - /// retries, so no retry will ever succeed to report the recovery — only ordinary traffic getting + /// retries, so no retry will ever succeed to report the recovery - only ordinary traffic getting /// through can. Without this, one bad afternoon stops retrying for good until somebody notices /// and resets it by hand. /// /// /// Only budgets that are actually used up. A partly-spent total is left alone. /// The cap exists to stop a flaky downstream being hammered, and that is precisely a downstream - /// where some messages succeed and others fail — crediting the total back on every ordinary + /// where some messages succeed and others fail - crediting the total back on every ordinary /// success would mean such a subscription never reaches its cap at all. /// /// @@ -125,7 +125,7 @@ public async Task ReleaseExhaustedBudgets(DateTime succeededFrom) if (policy?.Groups == null) return 0; // A group whose total is gone from the policy is left to Update and Delete to clean up, which - // they already do — releasing it here would be guessing at a cap that no longer exists. + // they already do - releasing it here would be guessing at a cap that no longer exists. var exhausted = spent .Where(u => policy.Groups.Any(g => g.Id == u.GroupId && g.Budget is { MaxAttemptsTotal: > 0 } @@ -147,28 +147,36 @@ public async Task ReleaseExhaustedBudgets(DateTime succeededFrom) /// /// One conditional UPDATE for the same reason the increment is one: several instances can /// discover the empty budget at the same moment, and a read-then-write would let each of them - /// decide it was the first. Exactly one caller updates a row, so exactly one alert is raised — + /// decide it was the first. Exactly one caller updates a row, so exactly one alert is raised - /// and because Reset deletes the row outright, clearing a budget re-arms the alert with it. /// private async Task ClaimExhaustionAlert(Guid groupId, int maxAttemptsTotal) { + var now = DateTime.UtcNow; var claimed = await dbContext.Set() .Where(u => u.SubscriptionId == subscriptionId && u.GroupId == groupId && u.AttemptsUsed >= maxAttemptsTotal && u.ExhaustedNotifiedOn == null) .ExecuteUpdateAsync(s => s - .SetProperty(u => u.ExhaustedNotifiedOn, _ => DateTime.UtcNow)) > 0; + .SetProperty(u => u.ExhaustedNotifiedOn, now)) > 0; return claimed ? RetryBudgetClaim.DeniedAndJustExhausted : RetryBudgetClaim.Denied; } - private async Task TryIncrement(BitweenDbContext db, Guid groupId, int maxAttemptsTotal) => - await db.Set() + // The time is taken here, not left in the expression: there EF turns DateTime.UtcNow into the + // database server's clock, and ReleaseExhaustedBudgets compares this against an exchange's + // StartedOn from the application's. Clocks a few milliseconds apart then made a success that + // came straight after an exhausting failure look older than it, and the budget stayed spent. + private async Task TryIncrement(BitweenDbContext db, Guid groupId, int maxAttemptsTotal) + { + var now = DateTime.UtcNow; + return await db.Set() .Where(u => u.SubscriptionId == subscriptionId && u.GroupId == groupId && u.AttemptsUsed < maxAttemptsTotal) .ExecuteUpdateAsync(s => s .SetProperty(u => u.AttemptsUsed, u => u.AttemptsUsed + 1) - .SetProperty(u => u.LastAttemptOn, _ => DateTime.UtcNow)) > 0; + .SetProperty(u => u.LastAttemptOn, now)) > 0; + } } diff --git a/SW.Bitween.Api/Services/RetryJob.cs b/SW.Bitween.Api/Services/RetryJob.cs index 1b3400c9..56587ef3 100644 --- a/SW.Bitween.Api/Services/RetryJob.cs +++ b/SW.Bitween.Api/Services/RetryJob.cs @@ -18,13 +18,14 @@ namespace SW.Bitween; /// Works through every retry that is already due rather than a hundred a minute, and commits one row /// at a time. Committing the batch in one go meant a single row that could not be carried out /// discarded the work of all the others and left their schedules in place, so the same batch came -/// back a minute later and failed the same way — no retry would ever have run again. +/// back a minute later and failed the same way - no retry would ever have run again. /// [ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService, ILogger logger) : IScheduledJob { private const int BatchSize = 100; + public const int MaxRunFailures = 5; public async Task Execute() { @@ -51,25 +52,46 @@ public async Task Execute() } catch (Exception ex) { - // Not "dropped": SaveChangesAsync commits before it publishes, so a failure in the - // publish leaves the replacement exchange committed and only its announcement - // missing. Saying the retry was dropped would send whoever reads this looking for - // an exchange that does exist. - logger.LogError(ex, - "The scheduled retry of xchange {XchangeId} did not complete; clearing its " - + "schedule so the queue keeps draining.", delayedRetry.Id); - - // Whatever the failed run left staged goes first — saving it would commit the very + // Whatever the failed run left staged goes first - saving it would commit the very // changes that failing was meant to prevent. dbContext.ChangeTracker.Clear(); - // Every row leaves the queue one way or another, which is what stops the loop above - // from meeting the same row again and turning the drain into a spin. - await dbContext.Set() - .Where(r => r.Id == delayedRetry.Id) - .ExecuteDeleteAsync(); + await AfterRunFailure(dbContext, delayedRetry, ex, logger); } } } } + + /// + /// What happens to a retry whose run threw. Every row leaves the pass one way or another, which + /// is what stops the loop from meeting the same row again and turning the drain into a spin: + /// moved later, so a passing failure - the database for a moment - doesn't lose the retry, or + /// dropped once it has kept failing. A run whose save did commit has already removed its row, + /// so neither statement touches it. + /// + public static async Task AfterRunFailure(BitweenDbContext dbContext, DelayedRetry delayedRetry, Exception ex, + ILogger logger) + { + var failures = delayedRetry.RunFailures + 1; + if (failures >= MaxRunFailures) + { + logger.LogError(ex, + "The scheduled retry of xchange {XchangeId} failed {Failures} times; clearing its schedule.", + delayedRetry.Id, failures); + await dbContext.Set() + .Where(r => r.Id == delayedRetry.Id) + .ExecuteDeleteAsync(); + return; + } + + var next = DateTime.UtcNow.AddMinutes(Math.Pow(2, failures)); + logger.LogWarning(ex, + "The scheduled retry of xchange {XchangeId} did not complete; trying again at {Next}.", + delayedRetry.Id, next); + await dbContext.Set() + .Where(r => r.Id == delayedRetry.Id) + .ExecuteUpdateAsync(u => u + .SetProperty(r => r.On, next) + .SetProperty(r => r.RunFailures, failures)); + } } diff --git a/SW.Bitween.Api/Services/RetryUsageReport.cs b/SW.Bitween.Api/Services/RetryUsageReport.cs index 39fcd9e4..8817849c 100644 --- a/SW.Bitween.Api/Services/RetryUsageReport.cs +++ b/SW.Bitween.Api/Services/RetryUsageReport.cs @@ -8,7 +8,7 @@ namespace SW.Bitween; /// -/// Builds the state of subscription-and-group pairs — spent budget, and where the pair's +/// Builds the state of subscription-and-group pairs - spent budget, and where the pair's /// budget-exhausted alert would go. /// /// @@ -25,7 +25,7 @@ public class RetryUsageReport(BitweenDbContext dbContext, AdapterSecretPropertie /// The pairs' subscriptions, with the names to report. /// Every group of the applicable policy; the unusable ones are dropped here. /// - /// The shared policy, or null for an inline one — which has no row to carry a + /// The shared policy, or null for an inline one - which has no row to carry a /// policy-level alert, so those pairs can only resolve to a group or an override. /// public async Task> Build( @@ -33,7 +33,7 @@ public async Task> Build( IReadOnlyList allGroups, RetryPolicy policy) { - // Only groups that allow retries have a budget to spend — and a group that can never spend + // Only groups that allow retries have a budget to spend - and a group that can never spend // one can never exhaust it, so it can never alert either. Listing those would invite // configuring an alert that cannot fire. A ceiling of zero counts as "never": TryConsume // denies it outright rather than claiming and exhausting it. No ceiling at all can never run @@ -52,8 +52,8 @@ public async Task> Build( .ToListAsync(); // What became of the alerts that were raised. The counter records only that one was - // claimed — claiming is what stops a redelivery sending it twice, and it happens before - // the send is tried — so whether anyone was actually told lives here instead, in the + // claimed - claiming is what stops a redelivery sending it twice, and it happens before + // the send is tried - so whether anyone was actually told lives here instead, in the // delivery log. The pair is reconstructed the same way the alert reached it: the exchange // names the integration, its result names the group. var alertLog = await ( @@ -76,7 +76,7 @@ on notification.XchangeId equals result.Id }) .ToListAsync(); - // One success is delivery, however many failures preceded it — the same rule the sender + // One success is delivery, however many failures preceded it - the same rule the sender // itself applies when it decides an alert has already gone out. Failing that, the most // recent failure is the outcome. var alertByPair = alertLog diff --git a/SW.Bitween.Api/Services/RunFlagUpdater.cs b/SW.Bitween.Api/Services/RunFlagUpdater.cs index 6001d230..5a8c6784 100644 --- a/SW.Bitween.Api/Services/RunFlagUpdater.cs +++ b/SW.Bitween.Api/Services/RunFlagUpdater.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -5,53 +7,53 @@ namespace SW.Bitween { - public class RunFlagUpdater(BitweenDbContext dbContext, BitweenOptions options) + public class RunFlagUpdater(BitweenDbContext dbContext, BitweenOptions options, ILogger logger) { - private readonly BitweenDbContext dbContext = dbContext; - private readonly string _dbType = options.DatabaseType; - + /// + /// Claims the subscription for one run. A single conditional UPDATE, so of two runners only + /// one changes the row, on every provider - the raw SQL this replaced read the flag with + /// SELECT ... FOR UPDATE outside a transaction on MySQL, which released the lock before the + /// UPDATE and let both through. + /// + /// False when another run holds it. public async Task MarkAsRunning(int id) { - var sqlUpdate = _dbType.ToLower() switch - { - "pgsql" => @"UPDATE infolink.subscription SET is_running = true - WHERE id = {0} and is_running = false - RETURNING is_running", - "mssql" => @"UPDATE Subscriptions SET IsRunning = 1 - OUTPUT INSERTED.IsRunning - WHERE Id = {0} and IsRunning = 0", - "mysql" => @"SELECT IsRunning FROM Subscriptions - WHERE Id = {0} and IsRunning = false - FOR UPDATE; - UPDATE Subscriptions SET IsRunning = true - WHERE Id = {0} and IsRunning = false", - _ => "" - }; - - var results = await dbContext.Set().FromSqlRaw(sqlUpdate, id).ToListAsync(); + var now = DateTime.UtcNow; + var staleBefore = now.AddMinutes(-options.StaleRunAfterMinutes); + + // A flag with no start time was set before RunningSince existed, and a flag older than + // the limit belongs to a run that died with its process. Either can be taken over. + var abandoned = await dbContext.Set().AsNoTracking() + .AnyAsync(s => s.Id == id && s.IsRunning && (s.RunningSince == null || s.RunningSince < staleBefore)); + + var claimed = await dbContext.Set() + .Where(s => s.Id == id && + (!s.IsRunning || s.RunningSince == null || s.RunningSince < staleBefore)) + .ExecuteUpdateAsync(u => u + .SetProperty(s => s.IsRunning, true) + .SetProperty(s => s.RunningSince, now)); - var result = results.SingleOrDefault(); - // result is null when is running is true - return result != null; + if (claimed == 1 && abandoned) + logger.LogWarning( + "Subscription {SubscriptionId} was still marked running from an earlier run that never finished; taking it over.", + id); + + return claimed == 1; } public async Task MarkAsIdle(int id) { - var sqlUpdate = _dbType.ToLower() switch - { - "pgsql" => @"UPDATE infolink.subscription SET is_running = false - WHERE id = {0}", - "mssql" => @"UPDATE Subscriptions SET IsRunning = 0 - WHERE Id = {0}", - "mysql" => @"UPDATE Subscriptions SET IsRunning = false - WHERE Id = {0}", - _ => "" - }; - ; - - await dbContext.Database.ExecuteSqlRawAsync(sqlUpdate, id); + await dbContext.Set() + .Where(s => s.Id == id) + .ExecuteUpdateAsync(u => u + .SetProperty(s => s.IsRunning, false) + .SetProperty(s => s.RunningSince, (DateTime?)null)); } + /// + /// No longer queried. Kept because it is part of the model: removing it would change every + /// provider's snapshot, and the PostgreSQL one with a migration. + /// public class RunningResult { public bool IsRunning { get; set; } diff --git a/SW.Bitween.Api/Services/SchedulerSeedService.cs b/SW.Bitween.Api/Services/SchedulerSeedService.cs index 15ef4fbf..436b08d2 100644 --- a/SW.Bitween.Api/Services/SchedulerSeedService.cs +++ b/SW.Bitween.Api/Services/SchedulerSeedService.cs @@ -29,7 +29,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // the second call grabs a connection still left in an aborted-transaction state from // the first, failing with "25P02: current transaction is aborted" while obtaining its // row lock. A fixed delay between the two calls was tried and proved unreliable (still - // reproduced on a fresh DB with the delay in place) — retrying the specific lock failure + // reproduced on a fresh DB with the delay in place) - retrying the specific lock failure // is correct instead, since it naturally waits however long is actually needed rather // than guessing, and still fails loudly if the lock truly can't be obtained after 3 tries. await ScheduleWithRetry(() => scheduleRepo.Schedule(options.RetryJobCron), nameof(RetryJob), stoppingToken); @@ -40,6 +40,13 @@ await ScheduleWithRetry( () => scheduleRepo.Schedule(options.InboundMessagePruneCron), nameof(InboundMessagePruneJob), stoppingToken); await ScheduleWithRetry(() => scheduleRepo.Schedule(options.ReceiveAttemptCleanupCron), nameof(ReceiveAttemptCleanupJob), stoppingToken); + await ScheduleWithRetry(() => scheduleRepo.Schedule(options.MetricsPruneCron), + nameof(Services.Metrics.MetricsPruneJob), stoppingToken); + await ScheduleWithRetry(() => scheduleRepo.Schedule(options.QueueSnapshotCron), + nameof(Services.Metrics.QueueSnapshotJob), stoppingToken); + // Always scheduled; it reads nothing beyond the subscriptions while none expects traffic. + await ScheduleWithRetry(() => scheduleRepo.Schedule(options.QuietSubscriptionsCron), + nameof(QuietSubscriptionsJob), stoppingToken); // Always scheduled; it does nothing while exchange retention is 0, so turning retention on in // Settings needs no re-scheduling. await ScheduleWithRetry(() => scheduleRepo.Schedule(options.ExchangeRetentionCron), @@ -81,7 +88,7 @@ private async Task ScheduleWithRetry(Func schedule, string jobName, Cancel catch (Quartz.Impl.AdoJobStore.LockException) when (attempt < maxAttempts) { logger.LogWarning( - "Transient lock error seeding {Job} (attempt {Attempt}/{Max}) — retrying", + "Transient lock error seeding {Job} (attempt {Attempt}/{Max}) - retrying", jobName, attempt, maxAttempts); await Task.Delay(200 * attempt, ct); } diff --git a/SW.Bitween.Api/Services/SecretColumnEncryptionPass.cs b/SW.Bitween.Api/Services/SecretColumnEncryptionPass.cs new file mode 100644 index 00000000..10bb2558 --- /dev/null +++ b/SW.Bitween.Api/Services/SecretColumnEncryptionPass.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Domain.DataSources; + +namespace SW.Bitween; + +/// +/// Rewrites the configuration rows saved before their credential columns were encrypted, so the +/// passwords already stored are encrypted too, not only the ones saved from now on. +/// +/// +/// +/// Waits a few minutes after startup first. During a rolling upgrade the previous version is still +/// running, and it cannot read an encrypted column; by then it has normally stopped. +/// +/// +/// Configuration only. Exchanges keep a copy of their subscription's properties, and new ones are +/// encrypted as they are created; those already stored age out with exchange retention rather than +/// rewriting the largest table in the database. +/// +/// +public class SecretColumnEncryptionPass(IServiceScopeFactory scopes, ILogger logger) + : BackgroundService +{ + private static readonly TimeSpan Delay = TimeSpan.FromMinutes(3); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!SecretColumnCipher.Enabled) + { + logger.LogWarning( + "Bitween:SettingsEncryptionKey is not set, so partner, adapter and data source credentials " + + "are stored unencrypted. Set it to encrypt them."); + return; + } + + try + { + await Task.Delay(Delay, stoppingToken); + var rewritten = await RunAsync(stoppingToken); + if (rewritten > 0) + logger.LogInformation("Re-saved {Count} configuration row(s) so their credentials are encrypted.", rewritten); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + } + catch (Exception ex) + { + logger.LogError(ex, "Encrypting stored credentials failed; it will be tried again on the next start."); + } + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + var total = 0; + total += await RewriteAsync(cancellationToken, nameof(Partner.AdapterProperties)); + total += await RewriteAsync(cancellationToken, nameof(GlobalAdapterValuesSet.Values)); + total += await RewriteAsync(cancellationToken, nameof(DataSource.Properties)); + total += await RewriteAsync(cancellationToken, + nameof(Subscription.HandlerProperties), nameof(Subscription.MapperProperties), + nameof(Subscription.ReceiverProperties), nameof(Subscription.ValidatorProperties)); + total += await RewriteAsync(cancellationToken, nameof(Notifier.HandlerProperties)); + total += await RewriteAsync(cancellationToken, "AlertHandlerProperties"); + total += await RewriteAsync(cancellationToken, "AlertHandlerProperties"); + return total; + } + + private async Task RewriteAsync(CancellationToken cancellationToken, params string[] properties) + where T : class + { + const int batchSize = 100; + var rewritten = 0; + for (var skip = 0; ; skip += batchSize) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Ordered by key, so the batches walk the table once. + var key = db.Model.FindEntityType(typeof(T))!.FindPrimaryKey()!.Properties[0].Name; + var batch = await db.Set() + .OrderBy(e => EF.Property(e, key)) + .Skip(skip).Take(batchSize) + .ToListAsync(cancellationToken); + if (batch.Count == 0) return rewritten; + + foreach (var entity in batch) + foreach (var property in properties) + db.Entry(entity).Property(property).IsModified = true; + + await db.SaveRewrittenValuesAsync(cancellationToken); + rewritten += batch.Count; + } + } +} diff --git a/SW.Bitween.Api/Services/SecurePasswordHasher.cs b/SW.Bitween.Api/Services/SecurePasswordHasher.cs index d0091326..df7cecdc 100644 --- a/SW.Bitween.Api/Services/SecurePasswordHasher.cs +++ b/SW.Bitween.Api/Services/SecurePasswordHasher.cs @@ -43,7 +43,7 @@ private static string Hash(string password, int iterations) var hash = Rfc2898DeriveBytes.Pbkdf2( password, salt, iterations, HashAlgorithmName.SHA256, V2HashSize); - // Salt first, then hash — Verify reads them back by the same offsets. + // Salt first, then hash - Verify reads them back by the same offsets. var hashBytes = new byte[SaltSize + V2HashSize]; salt.CopyTo(hashBytes, 0); hash.CopyTo(hashBytes, SaltSize); diff --git a/SW.Bitween.Api/Services/SeededAdministrator.cs b/SW.Bitween.Api/Services/SeededAdministrator.cs new file mode 100644 index 00000000..bc6010a9 --- /dev/null +++ b/SW.Bitween.Api/Services/SeededAdministrator.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain.Accounts; + +namespace SW.Bitween.Services; + +public static class SeededAdministrator +{ + public const int Id = 9999; + + // The password the migrations give the administrator they seed, published in this repository. + public const string PublishedPassword = "Mtm@dmin!2"; + + /// + /// Replaces the published password on the seeded administrator with the one configured in + /// Bitween:InitialAdminPassword. Runs after MigrateDatabase; once the password is + /// anything else this does nothing, so changing the configured value later has no effect. + /// + public static IHost SecureSeededAdministrator(this IHost host) + { + using var scope = host.Services.CreateScope(); + Secure( + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService()["Bitween:InitialAdminPassword"], + scope.ServiceProvider.GetRequiredService>()) + .GetAwaiter().GetResult(); + return host; + } + + /// + /// Every installation is seeded with this administrator, and while it holds the published + /// password whoever signs in first can choose a new one and own the instance. When no password + /// is configured, an installation with other accounts keeps today's behaviour (the password + /// must be changed at sign-in); one where this is the only account - a new installation - + /// refuses to start, since there is nobody else to set it up. + /// + public static async Task Secure(BitweenDbContext dbContext, string initialPassword, + ILogger logger) + { + var admin = await dbContext.Set().SingleOrDefaultAsync(a => a.Id == Id && !a.Deleted); + + if (admin is null || !HoldsPublishedPassword(admin)) + return; + + if (string.IsNullOrWhiteSpace(initialPassword)) + { + if (await dbContext.Set().AnyAsync(a => a.Id != Id && !a.Deleted)) + return; + + throw new InvalidOperationException( + "This is a new installation and Bitween:InitialAdminPassword is not configured. Set it " + + $"(env: Bitween__InitialAdminPassword) to the password for the administrator, {admin.Email}. " + + "It is read on startup only while that account still has the default password."); + } + + if (initialPassword == PublishedPassword) + throw new InvalidOperationException( + "Bitween:InitialAdminPassword is the default password, which is published in a public " + + "repository. Choose a different one."); + + var policy = new InlineValidator(); + policy.RuleFor(p => p).Password().OverridePropertyName("InitialAdminPassword"); + var result = policy.Validate(initialPassword); + if (!result.IsValid) + throw new InvalidOperationException( + "Bitween:InitialAdminPassword does not meet the password policy: " + + string.Join(" ", result.Errors.Select(e => e.ErrorMessage))); + + // Together or not at all: a new password stored without the deletion would never be retried, + // since the next start no longer finds the published one. A caller's transaction is used + // rather than nested. + await using var transaction = dbContext.Database.CurrentTransaction is null + ? await dbContext.Database.BeginTransactionAsync() + : null; + + admin.SetPassword(initialPassword); + await dbContext.SaveChangesAsync(); + + // A session opened with the published password would otherwise be refreshed into a full + // administrator now that the change-password requirement is cleared. + await dbContext.Set() + .Where(t => t.AccountId == admin.Id) + .ExecuteDeleteAsync(); + + if (transaction is not null) + await transaction.CommitAsync(); + + logger.LogInformation("Set the password of {Email} from Bitween:InitialAdminPassword.", admin.Email); + } + + private static bool HoldsPublishedPassword(Account admin) => + admin.Password is not null && + SecurePasswordHasher.IsHashSupported(admin.Password) && + SecurePasswordHasher.Verify(PublishedPassword, admin.Password); +} diff --git a/SW.Bitween.Api/Services/ServerlessAdapterDescriber.cs b/SW.Bitween.Api/Services/ServerlessAdapterDescriber.cs index ad938f91..d5cd47b4 100644 --- a/SW.Bitween.Api/Services/ServerlessAdapterDescriber.cs +++ b/SW.Bitween.Api/Services/ServerlessAdapterDescriber.cs @@ -16,7 +16,7 @@ namespace SW.Bitween; /// /// /// Asking is expensive out of all proportion to the answer: the package is downloaded and unzipped, -/// a dotnet child process is started, and the question goes over stdio — to be told a handful +/// a dotnet child process is started, and the question goes over stdio - to be told a handful /// of key names. So the answer is cached, one ask is shared by everyone who wants it at that moment, /// and only so many adapters may be running at once. /// @@ -35,7 +35,7 @@ public class ServerlessAdapterDescriber( /// /// How many adapters may be running at once, across the whole process. /// - /// Each one is a child process, and the callers are not coordinated — a screen loading four + /// Each one is a child process, and the callers are not coordinated - a screen loading four /// kinds of adapter is four requests, and there can be a request per user on top of that. Six /// is what one browser was already allowed per host before any of this was batched, so the /// server holds no more open at a time than it used to. @@ -58,7 +58,7 @@ public async Task> Describe(string adapterId) // Everyone who wants this adapter while it is being asked waits on the one ask, rather than // starting an identical child process of their own. Without this the cache does not help - // the case it most needs to — a cold start, when every request arrives at once. + // the case it most needs to - a cold start, when every request arrives at once. var asking = _asking.GetOrAdd(adapterId, id => new Lazy>>(() => Ask(id), LazyThreadSafetyMode.ExecutionAndPublication)); @@ -114,7 +114,7 @@ private async Task> Ask(string adapterId) IDictionary shared = new ReadOnlyDictionary(startupValues); - // Only a successful answer is cached — a boot that failed because, say, the adapter's + // Only a successful answer is cached - a boot that failed because, say, the adapter's // runtime is missing locally must not stick around as "this adapter has no properties". // // Held for as long as ServerlessService already serves a stale Hash from its own diff --git a/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs b/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs index 2005ae03..3d579d8a 100644 --- a/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs +++ b/SW.Bitween.Api/Services/Settings/SettingsCatalog.cs @@ -30,7 +30,7 @@ public enum SettingAccess ReadOnly, /// - /// Environment-owned and private: only whether a value is set is reported, never the value — + /// Environment-owned and private: only whether a value is set is reported, never the value - /// for credentials and keys that would be pointless to leak into the browser. /// Presence @@ -53,12 +53,12 @@ public sealed record SettingDefinition( Func Read, Action Write) { - /// Editable unless a definition says otherwise — see . + /// Editable unless a definition says otherwise - see . public SettingAccess Access { get; init; } = SettingAccess.Editable; /// /// Extra work needed to make a new value take effect, where assigning the property isn't - /// enough — rescheduling a job, re-declaring queues. Runs on the instance that saved it, + /// enough - rescheduling a job, re-declaring queues. Runs on the instance that saved it, /// after the value has been applied. /// public Func OnChange { get; init; } @@ -68,13 +68,13 @@ public sealed record SettingDefinition( } /// -/// Every setting the settings page shows, and — for the editable ones — the only keys the +/// Every setting the settings page shows, and - for the editable ones - the only keys the /// Settings table ever holds. /// /// The membership rule is about , not about being listed here. A /// setting is editable only if every consumer reads it from the options singleton per call, /// so a change takes effect immediately. Anything captured once during -/// Startup.ConfigureServices — the bus, CORS, storage, JWT signing, the DB provider — is +/// Startup.ConfigureServices - the bus, CORS, storage, JWT signing, the DB provider - is /// environment-owned and appears as (its value shown) or /// (only whether it's set), so an administrator can see what /// the instance is running on without being offered an edit that couldn't work. @@ -88,12 +88,12 @@ public static class SettingsCatalog { public static readonly IReadOnlyList All = [ - // ——— Documents & storage ——— + // --- Documents & storage --- // Exchange files are always private now; there's no switch for it. Readers without a login // get sealed links (see FileLinks). new("Bitween.DocumentPrefix", "Documents & storage", "Exchange files prefix", - "Where new exchange files are written in storage. The bucket's lifecycle rule for this prefix decides how long they're kept — temp30/… keeps them 30 days. Each exchange keeps the prefix it was written under, so a change only applies to exchanges created after it.", + "Where new exchange files are written in storage. The bucket's lifecycle rule for this prefix decides how long they're kept - temp30/… keeps them 30 days. Each exchange keeps the prefix it was written under, so a change only applies to exchanges created after it.", SettingKind.String, false, t => t.Bitween.DocumentPrefix, (t, v) => t.Bitween.DocumentPrefix = StoragePrefix(v)), @@ -107,7 +107,7 @@ public static class SettingsCatalog new("Bitween.ArchiveExchanges", "Documents & storage", "Archive before deleting", - "Copies each exchange — its details, and its files while storage still has them — to the archive before the retention job deletes it.", + "Copies each exchange - its details, and its files while storage still has them - to the archive before the retention job deletes it.", SettingKind.Boolean, false, t => Str(t.Bitween.ArchiveExchanges), (t, v) => t.Bitween.ArchiveExchanges = Bool(v)), @@ -123,7 +123,7 @@ public static class SettingsCatalog .Schedule(sp.GetRequiredService().ExchangeRetentionCron) }, - // ——— API behavior ——— + // --- API behavior --- new("Bitween.ApiCallSubscriptionResponseAcceptedStatusCode", "API behavior", "Accepted response status code", "Change this if a partner's API expects a different HTTP status code (instead of 202 Accepted) when their request has been queued for async processing.", @@ -149,12 +149,12 @@ public static class SettingsCatalog "The origins allowed to call this API with cookies attached. Read once when the CORS policy is built at startup.", SettingKind.String, t => Join(t.Bitween.CorsOrigins)), - // ——— Single sign-on (Microsoft) ——— + // --- Single sign-on (Microsoft) --- // Not secrets: these are public client identifiers, and the [Unprotect] Config endpoint // already serves them to anonymous visitors so the login page can offer the MS button. new("Bitween.MsalClientId", "Single sign-on (Microsoft)", "Azure AD client ID", - "Add this — together with the tenant ID and redirect URI below — if you want to let teammates sign in with a Microsoft account. All three are required for Microsoft sign-in to turn on.", + "Add this - together with the tenant ID and redirect URI below - if you want to let teammates sign in with a Microsoft account. All three are required for Microsoft sign-in to turn on.", SettingKind.String, false, t => t.Bitween.MsalClientId, (t, v) => t.Bitween.MsalClientId = v), @@ -168,22 +168,22 @@ public static class SettingsCatalog new("Bitween.MsalRedirectUri", "Single sign-on (Microsoft)", "Azure AD redirect URI", - "The URL Azure AD sends users back to after signing in — must match the redirect URI registered on the Azure AD app. Point it at this app's /blank.html (for example https://your-host/blank.html), not the app root: the root boots the admin UI, which navigates away and loses the sign-in code before the popup can hand it over. Required alongside the client ID and tenant ID.", + "The URL Azure AD sends users back to after signing in - must match the redirect URI registered on the Azure AD app. Point it at this app's /blank.html (for example https://your-host/blank.html), not the app root: the root boots the admin UI, which navigates away and loses the sign-in code before the popup can hand it over. Required alongside the client ID and tenant ID.", SettingKind.String, false, t => t.Bitween.MsalRedirectUri, (t, v) => t.Bitween.MsalRedirectUri = v), new("Bitween.DisableEmailPasswordLogin", "Single sign-on (Microsoft)", "Microsoft sign-in only", - "Turn this on to stop anyone signing in with an email and password — the sign-in page then offers Microsoft alone, and new teammates are added without a password. Only do this once Microsoft sign-in above is working, or nobody will be able to get in.", + "Turn this on to stop anyone signing in with an email and password - the sign-in page then offers Microsoft alone, and new teammates are added without a password. Only do this once Microsoft sign-in above is working, or nobody will be able to get in.", SettingKind.Boolean, false, t => Str(t.Bitween.DisableEmailPasswordLogin), (t, v) => t.Bitween.DisableEmailPasswordLogin = Bool(v)), - // ——— Adapters ——— + // --- Adapters --- new("Bitween.RebexLicenseKey", "Adapters", "Rebex license key", - "Add this if you want the native POP3 and FTP adapters, which are built on the Rebex library — without a key they aren't offered when picking a receiver or handler.", + "Add this if you want the native POP3 and FTP adapters, which are built on the Rebex library - without a key they aren't offered when picking a receiver or handler.", SettingKind.String, true, t => t.Bitween.RebexLicenseKey, (t, v) => t.Bitween.RebexLicenseKey = v), @@ -196,7 +196,23 @@ public static class SettingsCatalog "How long a custom adapter may run before the serverless runner gives up on it.", SettingKind.Number, t => t.Bitween.ServerlessCommandTimeout.ToString(CultureInfo.InvariantCulture)), - // ——— Reliability & jobs ——— + View("Bitween.NativeAdapterTimeout", "Adapters", "Built-in adapter timeout (seconds)", + "How long one call to a built-in adapter may take before the exchange fails as a timeout. 0 for no limit.", + SettingKind.Number, t => t.Bitween.NativeAdapterTimeout.ToString(CultureInfo.InvariantCulture)), + + View("Bitween.CircuitBreakerFailures", "Adapters", "Circuit breaker failures", + "Failures in a row (timeouts, refused connections, 429, 5xx) after which a built-in adapter's calls to that destination fail at once, as a 503, for a while. 0 switches it off.", + SettingKind.Number, t => t.Bitween.CircuitBreakerFailures.ToString(CultureInfo.InvariantCulture)), + + View("Bitween.CircuitBreakerWindowSeconds", "Adapters", "Circuit breaker window (seconds)", + "The failures must fall within this many seconds to open a destination's circuit.", + SettingKind.Number, t => t.Bitween.CircuitBreakerWindowSeconds.ToString(CultureInfo.InvariantCulture)), + + View("Bitween.CircuitBreakerOpenSeconds", "Adapters", "Circuit breaker cool-down (seconds)", + "How long an open circuit fails calls at once before one is let through to try the destination again.", + SettingKind.Number, t => t.Bitween.CircuitBreakerOpenSeconds.ToString(CultureInfo.InvariantCulture)), + + // --- Reliability & jobs --- new("Bitween.RetryJobCron", "Reliability & jobs", "Retry poll schedule", "How often Bitween looks for exchanges whose scheduled retry has come due, as a cron expression: second minute hour day-of-month month day-of-week. Saving re-schedules the job straight away.", @@ -210,7 +226,7 @@ public static class SettingsCatalog .Schedule(sp.GetRequiredService().RetryJobCron) }, - // ——— Messaging ——— + // --- Messaging --- // All environment-owned: the bus connection and its queue topology are built once, during // startup, so nothing here could take effect on a running instance. View("Bitween.QueuePrefix", "Messaging", "Queue name prefix", @@ -237,7 +253,7 @@ public static class SettingsCatalog "The password for the management account. Required alongside the URL and username.", t => t.Bitween.RabbitMqManagementPassword), - // ——— Database ——— + // --- Database --- View("Bitween.UseAzureManagedIdentity", "Database", "Use Azure managed identity", "Whether database connections authenticate with an Azure managed identity instead of a password in the connection string.", SettingKind.Boolean, t => Str(t.Bitween.UseAzureManagedIdentity)), @@ -246,15 +262,15 @@ public static class SettingsCatalog "Set only when a user-assigned identity is used; left unset, the system-assigned identity is.", t => t.Bitween.AzureManagedIdentityClientId), - // ——— Security ——— + // --- Security --- Presence("Bitween.SettingsEncryptionKey", "Security", "Settings encryption key", "Encrypts secret settings before they're stored. Without it, a secret can't be saved here at all and stays environment-only.", t => t.Bitween.SettingsEncryptionKey), - // ——— Brand & theme ——— + // --- Brand & theme --- new("Theme.PrimaryColor", "Brand & theme", "Primary color", - "Re-brands the whole app's accent color — buttons, links, active nav, focus rings — instantly, without waiting on a deploy.", + "Re-brands the whole app's accent color - buttons, links, active nav, focus rings - instantly, without waiting on a deploy.", SettingKind.Color, false, t => t.Theme.PrimaryColor, (t, v) => t.Theme.PrimaryColor = v), @@ -273,6 +289,20 @@ public static class SettingsCatalog t => t.Theme.TabTitle, (t, v) => t.Theme.TabTitle = v), + new("Theme.EnvironmentName", "Brand & theme", + "Environment name", + "Shown in a badge at the top of every page - \"Production\", \"Staging\" - so nobody mistakes one instance for another. Leave empty for no badge.", + SettingKind.String, false, + t => t.Theme.EnvironmentName, + (t, v) => t.Theme.EnvironmentName = v), + + new("Theme.EnvironmentColor", "Brand & theme", + "Environment badge color", + "The color of the environment badge. Red for production is a common choice.", + SettingKind.Color, false, + t => t.Theme.EnvironmentColor, + (t, v) => t.Theme.EnvironmentColor = v), + new("Theme.TabIcon", "Brand & theme", "Favicon URL", "The icon shown in the browser tab. Paste a URL to an .ico, .svg or .png.", @@ -315,28 +345,28 @@ public static class SettingsCatalog new("Theme.ShowFooter", "Brand & theme", "Show the footer", - "Turn this off to hide the footer everywhere — the copyright line and the website / LinkedIn / GitHub links below every page.", + "Turn this off to hide the footer everywhere - the copyright line and the website / LinkedIn / GitHub links below every page.", SettingKind.Boolean, false, t => Str(t.Theme.ShowFooter), (t, v) => t.Theme.ShowFooter = Bool(v)), new("Theme.LinkedinLink", "Brand & theme", "LinkedIn link", - "Add this if you want a LinkedIn link in the footer — leave blank to hide it.", + "Add this if you want a LinkedIn link in the footer - leave blank to hide it.", SettingKind.String, false, t => t.Theme.LinkedinLink, (t, v) => t.Theme.LinkedinLink = v), new("Theme.GithubLink", "Brand & theme", "GitHub link", - "Add this if you want a GitHub link in the footer — leave blank to hide it.", + "Add this if you want a GitHub link in the footer - leave blank to hide it.", SettingKind.String, false, t => t.Theme.GithubLink, (t, v) => t.Theme.GithubLink = v), new("Theme.WebsiteLink", "Brand & theme", "Website link", - "Add this if you want a company website link in the footer — leave blank to hide it.", + "Add this if you want a company website link in the footer - leave blank to hide it.", SettingKind.String, false, t => t.Theme.WebsiteLink, (t, v) => t.Theme.WebsiteLink = v), @@ -383,7 +413,7 @@ private static string Join(string[] values) => /// /// Rejected here rather than by the scheduler, because a bad expression that reached the table - /// would throw on the next boot — inside the background service that seeds every subscription's + /// would throw on the next boot - inside the background service that seeds every subscription's /// schedule, taking all of them down with it. /// private static string Cron(string value) => @@ -391,7 +421,7 @@ private static string Cron(string value) => ? value : throw new FormatException($"'{value}' is not a valid cron expression."); - /// Anything but an explicit "true" is off — matches how the UI posts checkboxes. + /// Anything but an explicit "true" is off - matches how the UI posts checkboxes. private static string HeaderName(string value) => PartnerKeyHeaders.Problem(value) is { } problem ? throw new FormatException(problem) : value.Trim(); @@ -417,8 +447,8 @@ private static string StoragePrefix(string value) if (!System.Text.RegularExpressions.Regex.IsMatch(prefix, "^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$")) throw new FormatException( "Use folder names of letters, digits, dots, dashes and underscores, separated by slashes."); - // "." and ".." are steps to anything that resolves the key as a path — a file link opened in a - // browser, say — and would put the archive outside archive/. + // "." and ".." are steps to anything that resolves the key as a path - a file link opened in a + // browser, say - and would put the archive outside archive/. return prefix.Split('/').Any(folder => folder.Trim('.').Length == 0) ? throw new FormatException("A folder name can't be only dots.") : prefix; diff --git a/SW.Bitween.Api/Services/Settings/SettingsProtector.cs b/SW.Bitween.Api/Services/Settings/SettingsProtector.cs index ab735cbe..3cbd4e4c 100644 --- a/SW.Bitween.Api/Services/Settings/SettingsProtector.cs +++ b/SW.Bitween.Api/Services/Settings/SettingsProtector.cs @@ -10,7 +10,7 @@ namespace SW.Bitween.Services; /// key twice produces different ciphertext, and tampering fails the authentication tag rather /// than decrypting to garbage. /// -/// The passphrase comes from — configuration +/// The passphrase comes from - configuration /// only, never the Settings table. When it isn't set, is false and /// secret settings stay out of the table entirely. /// @@ -55,7 +55,7 @@ public string Protect(string plaintext) } /// - /// Reverses . A value without the marker is returned untouched — that's + /// Reverses . A value without the marker is returned untouched - that's /// how a hand-written row, or one stored before a passphrase existed, still reads back. /// Throws when the value was written under a different passphrase or has been altered. /// diff --git a/SW.Bitween.Api/Services/Settings/SettingsService.cs b/SW.Bitween.Api/Services/Settings/SettingsService.cs index 0dfabe08..30c5da65 100644 --- a/SW.Bitween.Api/Services/Settings/SettingsService.cs +++ b/SW.Bitween.Api/Services/Settings/SettingsService.cs @@ -14,8 +14,8 @@ namespace SW.Bitween.Services; /// /// Configuration (env / appsettings) is read once per setting: the first boot after a key /// exists copies what configuration says into a row (), and from then -/// on configuration is ignored for that key. "Default" therefore means the product default — -/// the initializer on the options class — which is what a reset returns a setting to. +/// on configuration is ignored for that key. "Default" therefore means the product default - +/// the initializer on the options class - which is what a reset returns a setting to. /// /// /// Applying a value is just assigning the property: both option objects are singletons that every @@ -32,7 +32,7 @@ public class SettingsService /// public const string LegacyDocumentPrefixKey = "Bitween.LegacyDocumentPrefix"; - /// Pristine options — never mutated, only read, to answer "what ships in the box?". + /// Pristine options - never mutated, only read, to answer "what ships in the box?". private static readonly SettingsTarget CodeDefaults = new(new BitweenOptions(), new ThemeOptions()); private readonly SettingsTarget _target; @@ -40,7 +40,7 @@ public class SettingsService private readonly ILogger _logger; /// - /// What configuration bound at startup. This is the import source, not the default — + /// What configuration bound at startup. This is the import source, not the default - /// captured before any stored value is applied, and used only for keys with no row yet. /// private readonly Dictionary _configured; @@ -68,7 +68,7 @@ public static Dictionary DefaultsUnder(string prefix) => Setting /// /// What this setting is right now on the live options singletons. For an environment-owned - /// setting that's the whole story — it's what configuration bound at startup. + /// setting that's the whole story - it's what configuration bound at startup. /// public string LiveValue(SettingDefinition definition) => definition.Read(_target) ?? string.Empty; @@ -81,7 +81,7 @@ public string ToStored(SettingDefinition definition, string value) => definition.Secret ? _protector.Protect(value) : value; /// - /// Copies configuration into a row for any key that doesn't have one yet — the one-time + /// Copies configuration into a row for any key that doesn't have one yet - the one-time /// hand-off that lets an existing deployment keep the values it was configured with. Runs at /// startup only. /// @@ -148,7 +148,7 @@ public async Task Reload(BitweenDbContext dbContext) catch (Exception ex) { _logger.LogError(ex, - "Stored value for setting {Key} could not be read — it was likely written under a " + + "Stored value for setting {Key} could not be read - it was likely written under a " + "different {Option}. Leaving the running value untouched.", definition.Key, nameof(BitweenOptions.SettingsEncryptionKey)); } @@ -156,7 +156,7 @@ public async Task Reload(BitweenDbContext dbContext) } /// - /// Writes the first time this version starts — before anyone + /// Writes the first time this version starts - before anyone /// could have changed the prefix, so it's the one every earlier exchange was written under. Runs /// after , which has just applied the prefix imported from configuration. /// @@ -186,7 +186,7 @@ public async Task RememberLegacyDocumentPrefix(BitweenDbContext dbContext) /// /// Runs the real write against throwaway option objects, so the endpoint can reject "abc" - /// for a number before it reaches the database — without the live values ever seeing it. + /// for a number before it reaches the database - without the live values ever seeing it. /// public static void Validate(SettingDefinition definition, string value) => definition.Write(new SettingsTarget(new BitweenOptions(), new ThemeOptions()), value); diff --git a/SW.Bitween.Api/Services/SignInThrottle.cs b/SW.Bitween.Api/Services/SignInThrottle.cs new file mode 100644 index 00000000..748b8b76 --- /dev/null +++ b/SW.Bitween.Api/Services/SignInThrottle.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace SW.Bitween; + +/// +/// Locks an account against one address after repeated wrong passwords from it, leaving every +/// other address free to sign in. +/// +/// +/// +/// The account-wide lockout alone let anyone who knew an address keep its owner out: five wrong +/// guesses from anywhere and the real user could not sign in for fifteen minutes, again and +/// again. Counting per address stops guessing from that address without punishing the person who +/// owns the account. The account-wide lockout stays as a backstop at a higher threshold, for +/// guessing spread across many addresses. +/// +/// +/// Held in memory, per replica. A guesser whose attempts are spread over replicas gets a few more +/// tries before each replica locks them out, and the backstop still counts every attempt in the +/// database. Losing these counts on a restart only costs an attacker's progress being forgotten, +/// which the backstop also covers. +/// +/// +public class SignInThrottle +{ + public const int PerAddressLimit = 5; + public static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15); + + private const int PruneAbove = 10_000; + + private readonly ConcurrentDictionary<(int AccountId, string Address), Entry> _entries = new(); + + private sealed record Entry(int Failures, DateTime LastFailure, DateTime? LockedUntil); + + /// When the account stops being locked for this address, or null if it isn't. + public DateTime? LockedUntil(int accountId, string address, DateTime nowUtc) => + _entries.TryGetValue((accountId, address), out var entry) && entry.LockedUntil > nowUtc + ? entry.LockedUntil + : null; + + public void RegisterFailure(int accountId, string address, DateTime nowUtc) + { + _entries.AddOrUpdate((accountId, address), + _ => new Entry(1, nowUtc, null), + (_, entry) => + { + // Failures this far apart are not one guessing run. + var failures = nowUtc - entry.LastFailure > LockoutDuration ? 1 : entry.Failures + 1; + return failures >= PerAddressLimit + ? new Entry(0, nowUtc, nowUtc + LockoutDuration) + : new Entry(failures, nowUtc, entry.LockedUntil); + }); + + if (_entries.Count > PruneAbove) + Prune(nowUtc); + } + + /// A correct password from this address wipes its count. + public void Clear(int accountId, string address) => _entries.TryRemove((accountId, address), out _); + + /// An administrator's unlock lifts the account's lock on every address. + public void ClearAccount(int accountId) + { + foreach (var key in _entries.Keys.Where(k => k.AccountId == accountId).ToList()) + _entries.TryRemove(key, out _); + } + + private void Prune(DateTime nowUtc) + { + foreach (var (key, entry) in _entries) + if (nowUtc - entry.LastFailure > LockoutDuration && !(entry.LockedUntil > nowUtc)) + _entries.TryRemove(key, out _); + } +} diff --git a/SW.Bitween.Api/Services/SourceDocument.cs b/SW.Bitween.Api/Services/SourceDocument.cs index b90c9979..ec3e2c1c 100644 --- a/SW.Bitween.Api/Services/SourceDocument.cs +++ b/SW.Bitween.Api/Services/SourceDocument.cs @@ -12,7 +12,7 @@ namespace SW.Bitween; /// /// Reading source values: the values a response or bus gateway subscription takes from the -/// original document — the input of the exchange whose delivery got the response it runs on. +/// original document - the input of the exchange whose delivery got the response it runs on. /// /// /// @@ -22,10 +22,10 @@ namespace SW.Bitween; /// properties are read from. /// /// -/// A path is read the way a mapping reads one — dot-separated, keys matched exactly, never into a -/// list — with the mapper's own JSON and XML readers, so a path the mapping editor shows is a path +/// A path is read the way a mapping reads one - dot-separated, keys matched exactly, never into a +/// list - with the mapper's own JSON and XML readers, so a path the mapping editor shows is a path /// that works here. The document is read as JSON, or as XML when it starts with <. -/// Anything else — a CSV file, which is a list, or plain text — has no paths, so every value reads +/// Anything else - a CSV file, which is a list, or plain text - has no paths, so every value reads /// as missing. /// /// diff --git a/SW.Bitween.Api/Services/StorageAccess.cs b/SW.Bitween.Api/Services/StorageAccess.cs index 3d77f6dd..3b477d39 100644 --- a/SW.Bitween.Api/Services/StorageAccess.cs +++ b/SW.Bitween.Api/Services/StorageAccess.cs @@ -32,7 +32,7 @@ public class StorageAccess(IServiceProvider serviceProvider, BitweenOptions opti /// /// true when the file opened without credentials, false when the storage refused it, and - /// null when that can't be told — the storage address isn't reachable from here, say. + /// null when that can't be told - the storage address isn't reachable from here, say. /// public virtual async Task IsOpenToAnyoneAsync(bool refresh = false, CancellationToken cancellationToken = default) { @@ -107,8 +107,8 @@ public class StorageAccess(IServiceProvider serviceProvider, BitweenOptions opti } /// - /// Open means the file itself came back. Refused or not found means private; anything else — a server - /// error, some other page — can't be told either way. + /// Open means the file itself came back. Refused or not found means private; anything else - a server + /// error, some other page - can't be told either way. /// public static bool? Opens(HttpStatusCode status, string body) => status == HttpStatusCode.OK && body == ProbeText ? true diff --git a/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs b/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs index 4e12aa9f..d2241141 100644 --- a/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs +++ b/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs @@ -84,7 +84,7 @@ private async Task TryUnschedule(SubscriptionType type, int subId, Schedule sche } catch (SWValidationException) { - // Schedule didn't exist in Quartz — nothing to remove. + // Schedule didn't exist in Quartz - nothing to remove. } } } diff --git a/SW.Bitween.Api/Services/SystemPartnerKey.cs b/SW.Bitween.Api/Services/SystemPartnerKey.cs new file mode 100644 index 00000000..fb25dc40 --- /dev/null +++ b/SW.Bitween.Api/Services/SystemPartnerKey.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; + +namespace SW.Bitween.Services; + +/// +/// The SYSTEM partner is seeded with an API key that is published in this repository. Whoever +/// holds it can post any document type into content routing without signing in, so it must not +/// stay in place - but deployments that have been running for years may have systems that post +/// with it, so it cannot simply be changed under them. +/// +public static class SystemPartnerKey +{ + // The key the migrations give the SYSTEM partner, published in this repository. + public const string PublishedKey = "7facc758283844b49cc4ffd26a75b1de"; + + /// + /// Replaces the published key on the SYSTEM partner. Runs after MigrateDatabase; once + /// the key is anything else this does nothing, so changing the configured value later has no + /// effect - rotate it from the partner's page. + /// + private static bool IsPublished(ApiCredential credential) => + credential.Key == PublishedKey || credential.Key == PartnerKeyHash.Of(PublishedKey); + + public static IHost SecureSystemPartnerKey(this IHost host) + { + using var scope = host.Services.CreateScope(); + Secure( + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService()["Bitween:SystemPartnerKey"], + scope.ServiceProvider.GetRequiredService>()) + .GetAwaiter().GetResult(); + return host; + } + + /// + /// With Bitween:SystemPartnerKey configured, the published key is replaced by it, so an + /// operator can rotate the key and hand the new one to whatever posts with it in one step. + /// Without it, a new installation - the seeded administrator is the only account, as + /// judges it - gets a random key, since nothing can be + /// using the old one yet. An existing installation keeps the published key, so nothing that + /// posts with it breaks on upgrade, and every start logs that it is still in place. + /// + public static async Task Secure(BitweenDbContext dbContext, string configuredKey, ILogger logger) + { + var partner = await dbContext.Set() + .SingleOrDefaultAsync(p => p.Id == Partner.SystemId); + + var published = partner?.ApiCredentials.FirstOrDefault(IsPublished); + if (published is null) + return; + + string replacement; + if (!string.IsNullOrWhiteSpace(configuredKey)) + { + replacement = configuredKey.Trim(); + if (replacement == PublishedKey) + throw new InvalidOperationException( + "Bitween:SystemPartnerKey is the default key, which is published in a public repository. " + + "Choose a different one."); + if (replacement.Length < 32) + throw new InvalidOperationException( + "Bitween:SystemPartnerKey must be at least 32 characters."); + var replacementHash = PartnerKeyHash.Of(replacement); + if (await dbContext.Set().AnyAsync(p => + p.ApiCredentials.Any(c => c.Key == replacement || c.Key == replacementHash))) + throw new InvalidOperationException( + "Bitween:SystemPartnerKey is already in use by a partner. Choose a different one."); + } + else if (await dbContext.Set().AnyAsync(a => a.Id != SeededAdministrator.Id && !a.Deleted)) + { + logger.LogCritical( + "The SYSTEM partner still accepts the API key published in Bitween's public repository " + + "('{KeyName}'). Anyone who knows it can post documents into routing without signing in. " + + "Rotate it from the partner's page, or set Bitween:SystemPartnerKey " + + "(env: Bitween__SystemPartnerKey) and restart; update anything that posts with the old key first.", + published.Name); + return; + } + else + { + replacement = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(); + } + + // Credentials compare by name, so the old one has to go before one of the same name can be + // added. Together or not at all: a partner left with neither would lock out the caller. + await using var transaction = dbContext.Database.CurrentTransaction is null + ? await dbContext.Database.BeginTransactionAsync() + : null; + + var kept = partner.ApiCredentials.Where(c => !IsPublished(c)).ToList(); + partner.SetApiCredentials(kept); + await dbContext.SaveChangesAsync(); + + partner.SetApiCredentials(kept.Append(new ApiCredential(published.Name, replacement))); + await dbContext.SaveChangesAsync(); + + if (transaction is not null) + await transaction.CommitAsync(); + + logger.LogWarning( + string.IsNullOrWhiteSpace(configuredKey) + ? "Replaced the SYSTEM partner's published API key '{KeyName}' with a random one on this new installation." + : "Replaced the SYSTEM partner's published API key '{KeyName}' with Bitween:SystemPartnerKey.", + published.Name); + } +} diff --git a/SW.Bitween.Api/Services/ThemeOptions.cs b/SW.Bitween.Api/Services/ThemeOptions.cs index ea2300f7..a2af9980 100644 --- a/SW.Bitween.Api/Services/ThemeOptions.cs +++ b/SW.Bitween.Api/Services/ThemeOptions.cs @@ -2,7 +2,7 @@ namespace SW.Bitween.Services { /// /// Branding. Every value here is an editable setting, so these initializers are the product - /// defaults — what "reset to default" returns a setting to. A deployment can still seed + /// defaults - what "reset to default" returns a setting to. A deployment can still seed /// different values through configuration, but only once: the first boot imports them into the /// Settings table, and from then on the table is the only source (see SettingsService). /// @@ -13,10 +13,11 @@ public class ThemeOptions public string LoginLogo { get; set; } = "/brand/BitweenFull.svg"; public string BitweenLogo { get; set; } = "/brand/BitweenFull.svg"; + // The sentence the sign-in page shows when it isn't changed. It used to be a lowercase + // fragment meant to follow the word "Bitween", which the Settings page then showed as the + // default while the sign-in page showed something else. public string BitweenText { get; set; } = - "is all-in-one solution to solving integration with third parties, automating workflows " + - "with exchanges coming from all forms of requests, ranging from internal messages to " + - "files dumped on a server."; + "Receive, transform and deliver documents between you and your partners - with every exchange traced."; public string LinkedinLink { get; set; } = "https://www.linkedin.com/company/simplify9"; public string GithubLink { get; set; } = "https://github.com/simplify9"; @@ -33,5 +34,16 @@ public class ThemeOptions public string PrimaryColor { get; set; } = "#e3311d"; public bool ShowFooter { get; set; } = true; + + /// + /// Names this instance - "Production", "Staging", "Acme UAT" - in a badge at the top of + /// every page, so someone with three of them open in tabs can tell which one they are + /// about to change. Empty, the default: no badge, and nothing changes for a deployment + /// that never sets it. + /// + public string EnvironmentName { get; set; } = ""; + + /// The badge's color. Hex, e.g. #d97706. + public string EnvironmentColor { get; set; } = "#d97706"; } } diff --git a/SW.Bitween.Api/Services/TrafficWindow.cs b/SW.Bitween.Api/Services/TrafficWindow.cs new file mode 100644 index 00000000..54ecc092 --- /dev/null +++ b/SW.Bitween.Api/Services/TrafficWindow.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// A subscription's , checked and ready to ask: is an exchange due now? +/// +/// The window is a set of local days, each open from From to To (the whole day when +/// neither is set) in the expectation's time zone. Silence is only counted inside it: a quiet stretch +/// has to lie wholly in one open stretch of the window, so a partner that sends nothing overnight is +/// not reported at 08:01 because its last order came in at 21:55. +/// +/// +public sealed class TrafficWindow +{ + public const int MinimumMinutes = 5; + + // A week: past that, a whole-week window would never be found quiet anyway. + public const int MaximumMinutes = 7 * 24 * 60; + + static readonly string[] DayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + public ExpectTraffic Expectation { get; } + public TimeSpan Every { get; } + public TimeZoneInfo Zone { get; } + readonly bool[] _days; + readonly TimeSpan? _from, _to; + + TrafficWindow(ExpectTraffic expectation, bool[] days, TimeSpan? from, TimeSpan? to, TimeZoneInfo zone) + { + Expectation = expectation; + Every = TimeSpan.FromMinutes(expectation.EveryMinutes); + _days = days; + _from = from; + _to = to; + Zone = zone; + } + + /// + /// Checks an expectation and returns it in its stored form: days in week order with their usual + /// spelling, blanks as null. Null stays null. Throws with a + /// message that says what to change. + /// + public static ExpectTraffic Normalize(ExpectTraffic expectation) => + expectation == null ? null : Parse(expectation).Expectation; + + /// + public static TrafficWindow Parse(ExpectTraffic expectation) + { + ArgumentNullException.ThrowIfNull(expectation); + + if (expectation.EveryMinutes < MinimumMinutes || expectation.EveryMinutes > MaximumMinutes) + throw Invalid($"Expect an exchange every {MinimumMinutes} to {MaximumMinutes} minutes; " + + $"{expectation.EveryMinutes} is outside that. Bitween checks every few minutes, so a shorter gap can't be told apart."); + + var days = new bool[7]; + foreach (var day in (expectation.Days ?? []).Where(d => !string.IsNullOrWhiteSpace(d))) + { + var index = Array.FindIndex(DayNames, n => string.Equals(n, day.Trim(), StringComparison.OrdinalIgnoreCase)); + if (index < 0) + throw Invalid($"'{day}' isn't a day. Use Sun, Mon, Tue, Wed, Thu, Fri or Sat."); + days[index] = true; + } + // None named means every day. + var everyDay = !days.Any(d => d); + if (everyDay) Array.Fill(days, true); + + var from = Time(expectation.From, "From"); + var to = Time(expectation.To, "To"); + if (from.HasValue != to.HasValue) + throw Invalid("Give both From and To, or neither for the whole day."); + if (from.HasValue && to <= from) + throw Invalid($"To ({expectation.To}) has to be later than From ({expectation.From}). " + + "A window can't run past midnight; leave the times out to watch whole days."); + if (from.HasValue && expectation.EveryMinutes > (to.Value - from.Value).TotalMinutes) + throw Invalid($"Every {expectation.EveryMinutes} minutes is longer than the {expectation.From}-{expectation.To} window, " + + "so the subscription could never be found quiet."); + + var zoneId = string.IsNullOrWhiteSpace(expectation.TimeZone) ? null : expectation.TimeZone.Trim(); + TimeZoneInfo zone; + try + { + zone = zoneId == null ? TimeZoneInfo.Utc : TimeZoneInfo.FindSystemTimeZoneById(zoneId); + } + catch (Exception e) when (e is TimeZoneNotFoundException or InvalidTimeZoneException) + { + throw Invalid($"'{zoneId}' isn't a time zone Bitween knows. Use an IANA name such as Asia/Riyadh or Europe/London, or leave it empty for UTC."); + } + + var normalized = new ExpectTraffic + { + EveryMinutes = expectation.EveryMinutes, + Days = everyDay ? null : DayNames.Where((_, i) => days[i]).ToList(), + From = from?.ToString(@"hh\:mm"), + To = to?.ToString(@"hh\:mm"), + TimeZone = zoneId, + }; + return new TrafficWindow(normalized, days, from, to, zone); + } + + static TimeSpan? Time(string value, string field) + { + if (string.IsNullOrWhiteSpace(value)) return null; + if (TimeSpan.TryParseExact(value.Trim(), [@"hh\:mm", @"h\:mm"], CultureInfo.InvariantCulture, out var time) && + time < TimeSpan.FromDays(1)) + return time; + throw Invalid($"{field} '{value}' isn't a time of day. Use HH:mm, such as 08:00 or 22:30."); + } + + static SWValidationException Invalid(string message) => new("ExpectTraffic", message); + + /// + /// When the open stretch of the window that is in began, in UTC; null + /// when the window is closed. Consecutive whole days are one stretch, so Sunday's silence carries + /// into Monday; a window with times opens afresh each day. + /// + public DateTime? OpenedAt(DateTime utcNow) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(utcNow, Zone); + if (!_days[(int)local.DayOfWeek]) return null; + + if (_from.HasValue) + { + var time = local.TimeOfDay; + if (time < _from.Value || time >= _to!.Value) return null; + // Measured back from now rather than converted from local time, which a clock change + // can make ambiguous or skip altogether. + return utcNow - (time - _from.Value); + } + + var opened = utcNow - local.TimeOfDay; + for (var back = 1; back <= 7; back++) + { + if (!_days[((int)local.DayOfWeek - back + 7) % 7]) return opened; + opened -= TimeSpan.FromDays(1); + } + // Every day: the window never closes. + return DateTime.MinValue; + } + + /// + /// The moment an exchange must have started after for the subscription to count as not quiet at + /// ; null when it can't be quiet yet, because the window is closed or has + /// not been open for a whole . + /// + public DateTime? QuietUnlessAfter(DateTime utcNow) + { + var opened = OpenedAt(utcNow); + if (opened == null) return null; + var cutoff = utcNow - Every; + return opened <= cutoff ? cutoff : null; + } + + /// "every 15 minutes, Sun-Thu 08:00-22:00 Asia/Riyadh": the expectation in words. + public string Describe() + { + var days = DescribeDays(); + var hours = _from.HasValue ? $" {Expectation.From}-{Expectation.To}" : ""; + return $"every {Expectation.EveryMinutes} minutes, {days}{hours} {Expectation.TimeZone ?? "UTC"}"; + } + + string DescribeDays() + { + if (_days.All(d => d)) return "every day"; + // Runs of three or more read as a range, the rest as a list, both in week order. + var parts = new List(); + for (var i = 0; i < 7;) + { + if (!_days[i]) { i++; continue; } + var end = i; + while (end + 1 < 7 && _days[end + 1]) end++; + parts.Add(end - i >= 2 ? $"{DayNames[i]}-{DayNames[end]}" : string.Join(", ", DayNames[i..(end + 1)])); + i = end + 1; + } + return string.Join(", ", parts); + } +} diff --git a/SW.Bitween.Api/Services/XchangeService.cs b/SW.Bitween.Api/Services/XchangeService.cs index 7de31c70..f7d1e8f7 100644 --- a/SW.Bitween.Api/Services/XchangeService.cs +++ b/SW.Bitween.Api/Services/XchangeService.cs @@ -95,7 +95,7 @@ public async Task CreateXchange(Subscription subscription, Xchange xchange, Xcha /// /// A roll-up made before Bitween served its own file links lists storage URLs, which stop opening once - /// the bucket is private. Retrying one — with its old properties or reset ones — swaps each URL of an + /// the bucket is private. Retrying one - with its old properties or reset ones - swaps each URL of an /// exchange file in Bitween's bucket for a link Bitween serves, so the handler reading it still gets /// every file. Anything else is left as it was. /// @@ -138,6 +138,7 @@ public async Task CreateXchange(Document document, WorkGroup workGroup, source); await AddInputFile(xchange, file); dbContext.Add(xchange); + BitweenTelemetry.ExchangesCreated.Add(1, new KeyValuePair("kind", "routing")); return xchange; } @@ -147,13 +148,13 @@ public async Task CreateXchange(Subscription subscription, XchangeFile { // Callers that have no partner/globals context of their own (scheduled receivers, // aggregation, manual "create exchange", plain internal subscription fan-out) leave - // this null — resolve it here so {{globals.…}} always gets a chance to translate, + // this null - resolve it here so {{globals.…}} always gets a chance to translate, // instead of silently no-op'ing for whichever caller forgot to load it. globalAdapterValuesSets ??= await BitweenCache.ListGlobalAdapterValuesSetsAsync(); // And the same for the partner, for the same reason. Only a caller that learned the - // partner from somewhere other than the subscription — a bus gateway route, a partner - // calling an API gateway — has one to hand in; everyone else left it null and the + // partner from somewhere other than the subscription - a bus gateway route, a partner + // calling an API gateway - has one to hand in; everyone else left it null and the // subscription's own partner went unused, so every {{partner.…}} in its adapters was // written out literally and the handler ran against the template. This is the value // the Xchange is attributed to either way (see PartnerId below), so filling from it @@ -166,6 +167,7 @@ public async Task CreateXchange(Subscription subscription, XchangeFile globalAdapterValuesSets, source); await AddInputFile(xchange, file); dbContext.Add(xchange); + BitweenTelemetry.ExchangesCreated.Add(1, new KeyValuePair("kind", "subscription")); return xchange; } @@ -203,7 +205,7 @@ public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) var alreadyRetried = await FindRetryOf(xchange.Id); if (alreadyRetried != null) { - // Reached only if a manual retry got in first — the endpoint refuses that while a + // Reached only if a manual retry got in first - the endpoint refuses that while a // retry is scheduled, so it takes a race to arrive here. Dropped like the cases below // rather than left to throw: an exception here would leave the schedule in place and // the job would pick the same impossible retry up again on every pass, forever. @@ -219,7 +221,7 @@ public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) if (inputFile == null) { // The input is what a retry re-sends, so without it there is nothing to retry with. Handled - // like a missing subscription — drop the schedule and move on — but recorded on the result + // like a missing subscription - drop the schedule and move on - but recorded on the result // as well, because unlike a deleted subscription this needs someone to look into it. dbContext.Remove(delayedRetry); @@ -234,7 +236,7 @@ public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) } /// - /// The original input, or null when it cannot be read — deleted from storage, expired by a + /// The original input, or null when it cannot be read - deleted from storage, expired by a /// lifecycle rule, or storage itself unavailable. /// public async Task ReadInputFile(Xchange xchange) @@ -313,9 +315,17 @@ private async Task HandOnResponse(Xchange xchange, XchangeFile response /// Beside the body, not in it: any other reader of the message gets exactly the body it always /// did. Only the id, so what travels stays the same size however much a route reads. /// - private Task PublishResponse(Xchange xchange, XchangeFile responseFile) => - publish.Publish(xchange.ResponseMessageTypeName, responseFile.Data, - new Dictionary { [StartupValuesFiller.SourceXchangeBusValue] = xchange.Id }); + /// + /// Queues the response in the outbox, saved with the result. It used to be published before the + /// result was saved, so a save that then failed had already sent it - and the redelivery that + /// followed sent it again. + /// + private Task PublishResponse(Xchange xchange, XchangeFile responseFile) + { + dbContext.Add(new OutboxMessage(xchange.ResponseMessageTypeName, responseFile.Data, + new Dictionary { [StartupValuesFiller.SourceXchangeBusValue] = xchange.Id })); + return Task.CompletedTask; + } /// /// Fails a response or bus gateway subscription's exchange whose handler still has a @@ -324,7 +334,7 @@ private Task PublishResponse(Xchange xchange, XchangeFile responseFile) => /// /// The tokens are filled when the exchange is created, so one left over means the value wasn't /// there, and running on would hand the partner the token's text instead. Other types are never - /// given source values and are left alone — a template there may be reading a document field + /// given source values and are left alone - a template there may be reading a document field /// that happens to be called "source". /// private async Task EnsureSourceValuesFilled(Xchange xchange) @@ -381,7 +391,7 @@ private async Task RunMapper(Xchange xchange, XchangeFile xchangeFi if (xchange.MapperId == null) return xchangeFile; // A mapper that takes its context separately gets the payload exactly as it arrived. Anything - // else keeps the enrichment below, unchanged — every template already written reads + // else keeps the enrichment below, unchanged - every template already written reads // __partner__ out of the payload, so this is load-bearing behaviour, not an implementation // detail. Note the JToken.Parse: it runs before anything knows which mapper is configured, // and it throws on a payload that is not JSON, which is why a mapper that wants to handle @@ -413,9 +423,10 @@ private async Task RunMapper(Xchange xchange, XchangeFile xchangeFi } } - // Inject __globals__ — all global adapter values sets + // Inject __globals__ - all global adapter values sets // so templates can use {{ __globals__?.setId?.key }} - var globalSets = await dbContext.Set().ToListAsync(); + // From the cache, which every change to a set revokes: this ran a query per exchange. + var globalSets = await BitweenCache.ListGlobalAdapterValuesSetsAsync(); if (globalSets.Any(s => s.Values?.Count > 0)) { var globalsObj = new JObject(); @@ -438,7 +449,7 @@ private async Task RunMapper(Xchange xchange, XchangeFile xchangeFi // Check if it's a native adapter // No branching on the adapter's kind: the invoker decides which of the three runtimes - // owns this id — in-process, spawned, or a rented resident instance — and the pipeline + // owns this id - in-process, spawned, or a rented resident instance - and the pipeline // only says what it wants run. xchangeFile = await adapterInvoker.InvokeAsync( xchange.MapperId, AdapterRole.Mapper, nameof(IInfolinkHandler.Handle), xchangeFile, @@ -602,7 +613,7 @@ public string AdapterFileUrl(string xchangeId, string recordedPrefix, XchangeFil ?? throw new BitweenException("Bitween has no address to build links to exchange files on."); /// - /// The exchange a storage key belongs to, and which of its files it is — null when the key + /// The exchange a storage key belongs to, and which of its files it is - null when the key /// isn't exactly where one of an exchange's files lives. How a request naming a raw key is held to /// exchange files and nothing else in the bucket. /// @@ -630,9 +641,9 @@ public async Task GetFile(Xchange xchange, XchangeFileType type) } /// - /// Opens one of an exchange's files to read. A file that isn't there any more is reported as such — + /// Opens one of an exchange's files to read. A file that isn't there any more is reported as such - /// deleted by the bucket's retention rule when one covers it and the exchange is old enough, or else - /// missing — rather than as the storage provider's own error. + /// missing - rather than as the storage provider's own error. /// public async Task OpenFile(Xchange xchange, XchangeFileType type) { @@ -677,9 +688,31 @@ private async Task Process(XchangeMessage message) if (xchange == null) throw new BitweenException($"Xchange '{message.Id}' not found."); + // A message is delivered at least once - after a crash between commit and ack, or when the + // outbox publishes one whose first publish did go through. An exchange that already has a + // result has been processed; running it again would deliver it to the partner twice. + if (await dbContext.Set().AsNoTracking().AnyAsync(r => r.Id == xchange.Id)) + { + logger.LogInformation("Xchange {XchangeId} already has a result; ignoring a repeat delivery of it.", xchange.Id); + return; + } + + using var activity = BitweenTelemetry.ActivitySource.StartActivity("exchange.process"); + activity?.SetTag("bitween.exchange.id", xchange.Id); + activity?.SetTag("bitween.document.id", xchange.DocumentId); + activity?.SetTag("bitween.subscription.id", xchange.SubscriptionId); + var clock = System.Diagnostics.Stopwatch.StartNew(); + var outcome = "error"; + // For the metrics: which adapter a failure came from, and the failure itself. + var failedAt = SW.Bitween.Services.Metrics.FailedAt.Elsewhere; + Exception failure = null; + try { var inputFile = new XchangeFile(await GetFile(xchange, XchangeFileType.Input), xchange.InputName); + // Whatever way it arrived - bus, receiver, response - input that doesn't match its + // type's schema fails here, with what is wrong, before anything acts on it. + await DocumentSchema.Enforce(await BitweenCache.DocumentByIdAsync(xchange.DocumentId), inputFile.Data); var result = await filterService.Filter(xchange.DocumentId, inputFile); // Stored cut, so worth a line saying so: whoever set up the path most likely meant a @@ -696,12 +729,18 @@ private async Task Process(XchangeMessage message) workGroup = await BitweenCache.WorkGroupBySubscriptionIdAsync(xchange.SubscriptionId.Value); await EnsureSourceValuesFilled(xchange); if (xchange.MapperId == null) + { + failedAt = SW.Bitween.Services.Metrics.FailedAt.Handler; responseFile = await RunHandler(xchange, inputFile); + } else { + failedAt = SW.Bitween.Services.Metrics.FailedAt.Mapper; outputFile = await RunMapper(xchange, inputFile); + failedAt = SW.Bitween.Services.Metrics.FailedAt.Handler; responseFile = await RunHandler(xchange, outputFile); } + failedAt = SW.Bitween.Services.Metrics.FailedAt.Elsewhere; if (xchange.ResponseSubscriptionId != null && responseFile != null) responseXchange = await HandOnResponse(xchange, responseFile, inputFile); @@ -719,6 +758,7 @@ private async Task Process(XchangeMessage message) var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id); + await StampAsync(xchangeResult, xchange); dbContext.Add(xchangeResult); if (responseFile?.BadData == true) await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.BadResult, responseFile.Data, @@ -726,14 +766,157 @@ await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.BadResult, else await TryClearingRetryBudgetAfterSuccess(xchange); await dbContext.SaveChangesAsync(); + outcome = responseFile?.BadData == true ? "bad_response" : "success"; + // A bad response still means the target answered: the run of failures is over. + await TrackDeliveryHealth(xchange, null); } catch (Exception ex) { + activity?.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.GetType().Name); + DiscardUnsavedOutcome(xchange.Id, saveFailed: ex is DbUpdateException); + failure = ex; var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id, ex.ToString()); + await StampAsync(xchangeResult, xchange); dbContext.Add(xchangeResult); await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.Error, ex.ToString(), xchangeResult); await dbContext.SaveChangesAsync(); + await TrackDeliveryHealth(xchange, ex.ToString()); + } + finally + { + var outcomeTag = new KeyValuePair("outcome", outcome); + BitweenTelemetry.ExchangesProcessed.Add(1, outcomeTag); + BitweenTelemetry.ProcessingDuration.Record(clock.Elapsed.TotalSeconds, outcomeTag); + activity?.SetTag("bitween.outcome", outcome); + serviceProvider.GetService()?.Exchange( + xchange.StartedOn, xchange.SubscriptionId, xchange.PartnerId, + outcome switch + { + "success" => SW.Bitween.Services.Metrics.ExchangeOutcome.Succeeded, + "bad_response" => SW.Bitween.Services.Metrics.ExchangeOutcome.BadResponse, + _ => SW.Bitween.Services.Metrics.ExchangeOutcome.Failed, + }, + failedAt, xchange.RetryFor != null, xchange.ManualRetry, + (long)(DateTime.UtcNow - xchange.StartedOn).TotalMilliseconds, + xchange.InputSize, (outputFile?.Data.Length ?? 0) + (responseFile?.Data.Length ?? 0), + failure?.Message); + } + } + + /// + /// Writes on the result which node ran it, which adapter versions ran, and how long it took from + /// arriving. Never fails the exchange: a version that can't be read is left out. + /// + async Task StampAsync(XchangeResult result, Xchange xchange) + { + string mapperVersion = null, handlerVersion = null; + try + { + var catalog = serviceProvider.GetService(); + if (catalog != null) + { + mapperVersion = await RunVersionAsync(catalog, xchange.MapperId); + handlerVersion = await RunVersionAsync(catalog, xchange.HandlerId); + } + } + catch + { + // The versions are a convenience on the result, not part of it. + } + result.Stamp(SW.Bitween.Services.Metrics.MetricsRecorder.Node, mapperVersion, handlerVersion, + (long)(DateTime.UtcNow - xchange.StartedOn).TotalMilliseconds); + } + + static async Task RunVersionAsync(Services.Adapters.AdapterCatalog catalog, string adapterRef) + { + if (string.IsNullOrWhiteSpace(adapterRef) || NativeAdapterDiscoveryService.IsNative(adapterRef)) return null; + var (id, version) = SW.Serverless.Contract.Catalog.AdapterCatalogPaths.Split(adapterRef); + return version ?? (await catalog.GetAsync(id))?.Current; + } + + /// + /// Counts failed deliveries in a row for a subscription set to pause after some number of them, + /// and pauses it when the count is reached. Nothing for one that isn't. + /// + /// + /// Counted and compared in the database, not on a loaded copy: exchanges for one subscription + /// are processed side by side, and a read-then-write would lose counts and pause it twice. + /// Never throws - the exchange's own result is already saved and is what matters. + /// + private async Task TrackDeliveryHealth(Xchange xchange, string exception) + { + if (xchange.SubscriptionId is not { } subscriptionId) return; + try + { + var subscription = await BitweenCache.SubscriptionByIdAsync(subscriptionId); + if (subscription?.AutoPauseAfterFailures is not { } threshold) return; + + var subscriptions = dbContext.Set().Where(s => s.Id == subscriptionId); + if (exception is null) + { + await subscriptions.Where(s => s.ConsecutiveFailures != 0) + .ExecuteUpdateAsync(u => u + .SetProperty(s => s.ConsecutiveFailures, 0) + .SetProperty(s => s.LastException, (string)null)); + return; + } + + await subscriptions.ExecuteUpdateAsync(u => u + .SetProperty(s => s.ConsecutiveFailures, s => s.ConsecutiveFailures + 1) + .SetProperty(s => s.LastException, exception)); + + // Only the one exchange whose failure crosses the line pauses it, and alerts. + var paused = await subscriptions + .Where(s => s.PausedOn == null && s.ConsecutiveFailures >= threshold) + .ExecuteUpdateAsync(u => u + .SetProperty(s => s.PausedOn, DateTime.UtcNow) + .SetProperty(s => s.PausedAutomatically, true)); + if (paused == 0) return; + + logger.LogWarning( + "Subscription {SubscriptionId} paused after {Threshold} failed deliveries in a row; the last was xchange {XchangeId}.", + subscriptionId, threshold, xchange.Id); + BitweenTelemetry.SubscriptionsAutoPaused.Add(1); + + dbContext.Add(new OutboxMessage(nameof(SubscriptionAutoPausedEvent), + JsonConvert.SerializeObject(new SubscriptionAutoPausedEvent + { + SubscriptionId = subscriptionId, + XchangeId = xchange.Id, + Failures = threshold, + OccurredOn = DateTime.UtcNow + }))); + await dbContext.SaveChangesAsync(); + // What arrives next must see the pause, on every instance, not after the cache expires. + await BitweenCache.BroadcastRevoke(); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not track delivery health for subscription {SubscriptionId}.", subscriptionId); + } + } + + /// + /// Clears what the failed attempt left tracked before its error result is added. A success + /// result already added has the same key as the error one, so leaving it made the error save + /// throw too - the message went back on the queue and the handler ran again, delivering twice. + /// A response queued for a delivery that failed must not go out either. When it was the save + /// itself that failed, nothing else it tried to write can be trusted to save the second time. + /// + private void DiscardUnsavedOutcome(string xchangeId, bool saveFailed) + { + foreach (var entry in dbContext.ChangeTracker.Entries().ToList()) + { + if (entry.Entity is Xchange tracked && tracked.Id == xchangeId) continue; + + var discard = entry.Entity switch + { + XchangeResult result => result.Id == xchangeId, + OutboxMessage => entry.State == EntityState.Added, + _ => saveFailed && entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted, + }; + if (discard) entry.State = EntityState.Detached; } } @@ -795,7 +978,7 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul if (xchange.SubscriptionId == null) return; // A person asked for this attempt, so the policy stays out of it. Otherwise pressing Retry - // spends a slot of the group's shared total — the budget meant for unattended retries — and + // spends a slot of the group's shared total - the budget meant for unattended retries - and // can be what finally exhausts it and raises the alert. Recorded rather than skipped // silently, so the absence of a follow-up attempt has a visible reason. if (xchange.ManualRetry) @@ -823,6 +1006,16 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul new RetryGroupBudget(dbContext, serviceProvider, xchange.SubscriptionId.Value)); var attemptIndex = await CountRetryChainDepth(xchange); + + // A ceiling no policy can lift, checked before the policy so it spends no budget. + if (attemptIndex >= BitweenSettings.MaxRetryChainDepth) + { + xchangeResult.SetRetryBlocked( + $"This exchange has been retried {attemptIndex} times, the most Bitween allows " + + $"(Bitween:MaxRetryChainDepth)."); + return; + } + var decision = await evaluator.Evaluate(resultType, content, attemptIndex); // Which group owned this failure, so the group's retries can later be listed without @@ -834,7 +1027,7 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul dbContext.Add(new DelayedRetry { Id = xchange.Id, - On = DateTime.UtcNow + decision.Delay + On = DateTime.UtcNow + Jittered(decision.Delay) }); else // A policy applied but refused. Recorded so an exhausted budget is distinguishable @@ -852,6 +1045,15 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul decision.MatchedGroup.Budget!.MaxAttemptsTotal!.Value); } + /// + /// The policy's delay, give or take a tenth. Everything that failed in the same outage used to + /// come back at the same instant, and fail together again against a partner still recovering. + /// + internal static TimeSpan Jittered(TimeSpan delay) => + delay <= TimeSpan.Zero + ? delay + : TimeSpan.FromMilliseconds(delay.TotalMilliseconds * (0.9 + Random.Shared.NextDouble() * 0.2)); + /// /// The retry, if any, already made from . /// @@ -868,8 +1070,8 @@ private Task FindRetryOf(string xchangeId) => /// anything, and no way to say which one "the retry" of the original was. /// /// - /// Enforced here rather than at each endpoint so that every way of asking for a retry — by - /// hand, in bulk, or by a retry policy coming due — is held to it. Not enforced by a unique + /// Enforced here rather than at each endpoint so that every way of asking for a retry - by + /// hand, in bulk, or by a retry policy coming due - is held to it. Not enforced by a unique /// index as well: exchanges retried before this rule existed can already have forked, and an /// index that will not create over the data it inherits is worse than no index. Two retries of /// the same exchange committed at the very same moment can therefore still both pass this @@ -881,7 +1083,7 @@ private async Task EnsureNotAlreadyRetried(string xchangeId) if (existing != null) throw new SWValidationException("ALREADY_RETRIED", $"This exchange has already been retried, as exchange {existing}. Retry that attempt " + - "instead — an exchange is only retried once, so that its attempts stay a single chain."); + "instead - an exchange is only retried once, so that its attempts stay a single chain."); } private async Task CountRetryChainDepth(Xchange xchange) @@ -924,7 +1126,7 @@ async Task CreateXchangesForHits(Xchange xchange, FilterResult result, XchangeFi // Bus-gateway routes: run the assigned subscription with the route's optional partner values, // reusing the same xchange path the API gateway uses (partner + globals injection). - var globalAdapterValuesSets = await dbContext.Set().ToArrayAsync(); + var globalAdapterValuesSets = await BitweenCache.ListGlobalAdapterValuesSetsAsync(); foreach (var hit in result.GatewayHits) { var subscription = await BitweenCache.SubscriptionByIdAsync(hit.SubscriptionId); @@ -975,7 +1177,7 @@ async Task SourceFor(Subscription subscription) /// /// An exchange's input, for its source values, or null when it can't be read: the exchange isn't - /// in this Bitween — another instance on the same bus published the message — or its file is gone. + /// in this Bitween - another instance on the same bus published the message - or its file is gone. /// private async Task ReadSourceDocument(string xchangeId) { @@ -1036,10 +1238,26 @@ private async Task NotifyResult(Notifier notifier, XchangeResult xchangeResult, if (notifier?.HandlerId == null) return; + // Sent already: the result event was delivered again. Notifying twice was the symptom. + if (await dbContext.Set().AsNoTracking() + .AnyAsync(n => n.XchangeId == xchangeResult.Id && n.NotifierId == notifier.Id && (n.Success || n.Suppressed))) + return; + var xchange = await dbContext.FindAsync(xchangeResult.Id); var subscription = await BitweenCache.SubscriptionByIdAsync(xchange!.SubscriptionId!.Value); var document = await BitweenCache.DocumentByIdAsync(xchange.DocumentId); + // One message for an outage, not one per failure: within the quiet window after a send for + // the same subscription and outcome, this one is recorded but not sent. + var (lastSent, suppressedSince) = await QuietWindow(notifier, subscription.Id, xchangeResult); + if (lastSent is not null && BitweenSettings.NotifierQuietMinutes > 0 && + lastSent > DateTime.UtcNow.AddMinutes(-BitweenSettings.NotifierQuietMinutes)) + { + dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name) { Suppressed = true, Success = false }); + await dbContext.SaveChangesAsync(); + return; + } + var notificationData = new XchangeResultNotification { Id = xchangeResult.Id, @@ -1053,27 +1271,42 @@ private async Task NotifyResult(Notifier notifier, XchangeResult xchangeResult, SubscriptionId = subscription.Id, DocumentName = document.Name, DocumentId = document.Id, - CorrelationId = xchange.CorrelationId + CorrelationId = xchange.CorrelationId, + SuppressedSinceLast = suppressedSince }; - var handlerProperties = notifier.HandlerProperties.ToDictionary(); - handlerProperties["xchangeid"] = xchangeResult.Id; + var lastError = await NotifierDelivery.SendAsync(adapterInvoker, notifier, notificationData, + xchangeResult.Id, correlationId); - try - { - await adapterInvoker.InvokeAsync( - notifier.HandlerId, AdapterRole.Handler, nameof(IInfolinkHandler.Handle), - new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id), - handlerProperties, correlationId); + dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name, lastError?.ToString())); + await dbContext.SaveChangesAsync(); + } - dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name)); - } - catch (Exception ex) - { - dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name, ex.ToString())); - } + /// + /// When this notifier last sent for the subscription and this outcome, and how many it has held + /// back since. + /// + private async Task<(DateTime? LastSent, int SuppressedSince)> QuietWindow(Notifier notifier, int subscriptionId, + XchangeResult result) + { + var sameOutcome = + from n in dbContext.Set().AsNoTracking() + join x in dbContext.Set() on n.XchangeId equals x.Id + join r in dbContext.Set() on n.XchangeId equals r.Id + where n.NotifierId == notifier.Id && x.SubscriptionId == subscriptionId && + r.Success == result.Success && r.ResponseBad == result.ResponseBad + select n; + + var lastSent = await sameOutcome.Where(n => n.Success) + .OrderByDescending(n => n.FinishedOn) + .Select(n => (DateTime?)n.FinishedOn) + .FirstOrDefaultAsync(); - await dbContext.SaveChangesAsync(); + var suppressed = lastSent is null + ? 0 + : await sameOutcome.CountAsync(n => n.Suppressed && n.FinishedOn > lastSent); + + return (lastSent, suppressed); } public async Task Process(SubscriptionUnpausedEvent message) @@ -1082,21 +1315,34 @@ public async Task Process(SubscriptionUnpausedEvent message) if (subscription == null || subscription.Inactive || subscription.PausedOn != null) return; - var xchangesDetails = await dbContext.Set().Where(x => x.SubscriptionId == subscription.Id) - .ToListAsync(); - - foreach (var xchangeDetails in xchangesDetails) + // In batches, each committed on its own. Loading the whole backlog - payloads and all - and + // committing it in one go could run a pod out of memory on a long pause, and a failure + // part-way redid all of it on redelivery. Each batch removes what it released, so a + // redelivery carries on from where this stopped. + const int batchSize = 50; + while (true) { - var file = new XchangeFile(xchangeDetails.Data, xchangeDetails.FileName, xchangeDetails.BadData); - var partner = xchangeDetails.PartnerId.HasValue - ? await dbContext.FindAsync(xchangeDetails.PartnerId.Value) - : null; - await CreateXchange(subscription, file, xchangeDetails.References, xchangeDetails.CorrelationId, partner, - source: xchangeDetails.Source); - dbContext.Remove(xchangeDetails); - } + var batch = await dbContext.Set() + .Where(x => x.SubscriptionId == subscription.Id) + .OrderBy(x => x.Id) + .Take(batchSize) + .ToListAsync(); + if (batch.Count == 0) break; + + foreach (var xchangeDetails in batch) + { + var file = new XchangeFile(xchangeDetails.Data, xchangeDetails.FileName, xchangeDetails.BadData); + var partner = xchangeDetails.PartnerId.HasValue + ? await dbContext.FindAsync(xchangeDetails.PartnerId.Value) + : null; + await CreateXchange(subscription, file, xchangeDetails.References, xchangeDetails.CorrelationId, partner, + source: xchangeDetails.Source); + dbContext.Remove(xchangeDetails); + } - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); + dbContext.ChangeTracker.Clear(); + } } public async Task> GetMessageTypeNames() diff --git a/SW.Bitween.Cli/BitweenApi.cs b/SW.Bitween.Cli/BitweenApi.cs new file mode 100644 index 00000000..3cb6519f --- /dev/null +++ b/SW.Bitween.Cli/BitweenApi.cs @@ -0,0 +1,283 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace SW.Bitween.Cli; + +/// A Bitween's API, refused or answered. +public class BitweenApiException(string message, HttpStatusCode? status = null) : Exception(message) +{ + public HttpStatusCode? Status { get; } = status; +} + +/// +/// Calls to one Bitween's API as the signed-in member: the access token goes with each call, and is +/// renewed with the refresh token when it has expired, the way the browser renews it - the renewed +/// tokens are saved back to the profile. Or, for a job with no profile, as a service account: its +/// token is traded for a session, and traded again whenever that runs out; nothing is saved. +/// +public class BitweenApi : IDisposable +{ + readonly HttpClient http; + readonly Profiles profiles; + readonly string profileName; + readonly string serviceToken; + readonly SemaphoreSlim renewing = new(1, 1); + Profile profile; + + public BitweenApi(Profiles profiles, string profileName, Profile profile, HttpMessageHandler handler = null) + { + this.profiles = profiles; + this.profileName = profileName; + this.profile = profile; + http = new HttpClient(handler ?? Handler(profile.Insecure)) + { + BaseAddress = new Uri(profile.Url.TrimEnd('/') + "/"), + Timeout = TimeSpan.FromMinutes(10), + }; + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("bitween-cli", typeof(BitweenApi).Assembly.GetName().Version?.ToString() ?? "0")); + } + + /// As a service account, from its token: BITWEEN_URL and BITWEEN_TOKEN. + public BitweenApi(string url, string serviceToken, bool insecure = false, HttpMessageHandler handler = null) + : this(null, null, new Profile { Url = url.TrimEnd('/'), Insecure = insecure }, handler) + { + this.serviceToken = serviceToken; + } + + public string Url => profile.Url; + + public bool AsService => serviceToken != null; + + /// + /// Cookies are handled by hand, so the refresh token is only ever where the profile keeps it. + /// Insecure skips certificate checks, for a Bitween on a self-signed development certificate. + /// + /// For tests: the handler every call goes through, an in-process Bitween's, say. + internal static Func HandlerOverride; + + static HttpMessageHandler Handler(bool insecure) + { + if (HandlerOverride != null) return HandlerOverride(); + var handler = new HttpClientHandler { UseCookies = false }; + if (insecure) + { + Console.Error.WriteLine("Warning: not checking the Bitween's certificate (--insecure)."); + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + return handler; + } + + /// Signs in with an email and password, and returns the profile to keep. + public static async Task SignInAsync(string url, string email, string password, bool insecure = false, HttpMessageHandler handler = null) + { + using var http = new HttpClient(handler ?? Handler(insecure)) { BaseAddress = new Uri(url.TrimEnd('/') + "/") }; + var response = await http.PostAsJsonAsync("api/accounts/login", new { Username = email, Password = password }); + var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) throw new BitweenApiException(Refusal(response.StatusCode, body), response.StatusCode); + + var answer = JsonNode.Parse(body); + if ((bool?)answer?["mustChangePassword"] == true) + throw new BitweenApiException("This account must change its password first: sign in to Bitween in a browser, change it, then sign in here."); + return new Profile + { + Url = url.TrimEnd('/'), + Email = email, + AccessToken = (string)answer?["jwt"], + RefreshToken = RefreshTokenIn(response) ?? throw new BitweenApiException("Bitween signed in but sent no refresh token; is this a Bitween?"), + Insecure = insecure, + }; + } + + /// Trades a code from the browser, with this process's verifier, for a profile to keep. + public static async Task RedeemAsync(string url, string code, string verifier, bool insecure = false, HttpMessageHandler handler = null) + { + using var http = new HttpClient(handler ?? Handler(insecure)) { BaseAddress = new Uri(url.TrimEnd('/') + "/") }; + var response = await http.PostAsJsonAsync("api/accounts/clitoken", new { Code = code, CodeVerifier = verifier }); + var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) throw new BitweenApiException(Refusal(response.StatusCode, body), response.StatusCode); + + var answer = JsonNode.Parse(body); + return new Profile + { + Url = url.TrimEnd('/'), + Email = (string)answer?["email"], + AccessToken = (string)answer?["jwt"] ?? throw new BitweenApiException("Bitween answered without a session; is this a Bitween?"), + RefreshToken = (string)answer?["refreshToken"], + Insecure = insecure, + }; + } + + /// + /// Ends the session on the Bitween, so its refresh token stops working there and not only here. + /// Best effort: a Bitween that can't be reached, or that predates this, just isn't told. + /// + public static async Task EndSessionAsync(Profile profile, HttpMessageHandler handler = null) + { + if (string.IsNullOrEmpty(profile.RefreshToken)) return false; + try + { + using var http = new HttpClient(handler ?? Handler(profile.Insecure)) + { + BaseAddress = new Uri(profile.Url.TrimEnd('/') + "/"), + Timeout = TimeSpan.FromSeconds(15), + }; + using var response = await http.PostAsJsonAsync("api/accounts/logout", new { RefreshToken = profile.RefreshToken }); + return response.IsSuccessStatusCode; + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) + { + return false; + } + } + + public async Task GetAsync(string path) => await SendAsync(() => new HttpRequestMessage(HttpMethod.Get, path)); + + public async Task PostAsync(string path, object body) => + await SendAsync(() => new HttpRequestMessage(HttpMethod.Post, path) { Content = JsonContent.Create(body) }); + + public async Task DeleteAsync(string path) => await SendAsync(() => new HttpRequestMessage(HttpMethod.Delete, path)); + + public async Task PostFileAsync(string path, string file, string contentType) + { + return await SendAsync(() => + { + var content = new StreamContent(File.OpenRead(file)); + content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + return new HttpRequestMessage(HttpMethod.Post, path) { Content = content }; + }); + } + + async Task SendAsync(Func make) + { + var token = profile.AccessToken; + if (Expired(token)) await RenewAsync(token); + var response = await SendOnceAsync(make()); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + // Expired between the check and the call, or revoked: renew once and try again. + response.Dispose(); + await RenewAsync(token); + response = await SendOnceAsync(make()); + } + + using (response) + { + var text = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) throw new BitweenApiException(Refusal(response.StatusCode, text), response.StatusCode); + return string.IsNullOrWhiteSpace(text) ? null : JsonNode.Parse(text); + } + } + + Task SendOnceAsync(HttpRequestMessage request) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", profile.AccessToken); + return http.SendAsync(request); + } + + /// + /// A new session, unless another call renewed it since was read: calls in + /// parallel that all find it expired renew it once between them. + /// + async Task RenewAsync(string stale) + { + await renewing.WaitAsync(); + try + { + if (profile.AccessToken != stale && !Expired(profile.AccessToken)) return; + if (serviceToken != null) await TradeAsync(); + else await RefreshAsync(); + } + finally + { + renewing.Release(); + } + } + + async Task TradeAsync() + { + var response = await http.PostAsJsonAsync("api/accounts/servicetoken", new { Token = serviceToken }); + var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + throw new BitweenApiException($"{profile.Url} refused the service token: {Refusal(response.StatusCode, body)}", response.StatusCode); + profile.AccessToken = (string)JsonNode.Parse(body)?["jwt"]; + } + + async Task RefreshAsync() + { + if (string.IsNullOrEmpty(profile.RefreshToken)) + throw new BitweenApiException($"The session with {profile.Url} has ended; sign in again with bitween login {profile.Url}."); + var response = await http.PostAsJsonAsync("api/accounts/login", new { RefreshToken = profile.RefreshToken }); + var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + throw new BitweenApiException($"The session with {profile.Url} has ended; sign in again with bitween login {profile.Url}.", response.StatusCode); + + profile.AccessToken = (string)JsonNode.Parse(body)?["jwt"]; + profile.RefreshToken = RefreshTokenIn(response) ?? profile.RefreshToken; + profiles.Put(profileName, profile, makeCurrent: false); + } + + static string RefreshTokenIn(HttpResponseMessage response) + { + if (!response.Headers.TryGetValues("Set-Cookie", out var cookies)) return null; + foreach (var cookie in cookies) + { + var first = cookie.Split(';')[0]; + var eq = first.IndexOf('='); + if (eq > 0 && first[..eq].Trim() == "refresh_token") return Uri.UnescapeDataString(first[(eq + 1)..].Trim()); + } + return null; + } + + /// Whether a JWT has expired, or will within the minute; unreadable counts as expired. + internal static bool Expired(string jwt) + { + try + { + var payload = jwt?.Split('.')[1]; + if (payload == null) return true; + payload = payload.Replace('-', '+').Replace('_', '/'); + payload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='); + var exp = (long?)JsonNode.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(payload)))?["exp"]; + return exp == null || DateTimeOffset.FromUnixTimeSeconds(exp.Value) < DateTimeOffset.UtcNow.AddMinutes(1); + } + catch (Exception ex) when (ex is FormatException or JsonException or IndexOutOfRangeException) + { + return true; + } + } + + /// What Bitween said, as a sentence: its validation messages, or the status. + static string Refusal(HttpStatusCode status, string body) + { + try + { + if (JsonNode.Parse(body) is JsonObject problems) + { + // Validation answers as { field: [messages] }, or problem details with "errors". Problem + // details without them say only the status, which the sentences below say better. + var errors = problems["errors"] as JsonObject ?? (problems.ContainsKey("status") ? new JsonObject() : problems); + var messages = errors.SelectMany(e => e.Value is JsonArray a ? a.Select(x => (string)x) : [e.Value?.ToString()]) + .Where(m => !string.IsNullOrWhiteSpace(m)).ToList(); + if (messages.Count > 0) return string.Join(" ", messages); + } + } + catch (JsonException) { } + return status switch + { + HttpStatusCode.Unauthorized => "Bitween refused the sign-in.", + HttpStatusCode.Forbidden => "Your Bitween account doesn't have the permission this needs.", + HttpStatusCode.NotFound => "Bitween has no such thing.", + _ => $"Bitween answered {(int)status}{(string.IsNullOrWhiteSpace(body) ? "" : ": " + body.Trim())}", + }; + } + + public void Dispose() + { + http.Dispose(); + renewing.Dispose(); + } +} diff --git a/SW.Bitween.Cli/BrowserSignIn.cs b/SW.Bitween.Cli/BrowserSignIn.cs new file mode 100644 index 00000000..0f7d62c2 --- /dev/null +++ b/SW.Bitween.Cli/BrowserSignIn.cs @@ -0,0 +1,157 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace SW.Bitween.Cli; + +/// +/// Signing in through the browser, so any account can sign in however it signs in to Bitween, +/// Microsoft included, with nothing to configure. The admin UI asks the member to confirm, then +/// hands a one-time code back to a listener on this machine's loopback; the code is only good with +/// the verifier this process keeps (PKCE). Without a browser on this machine, the page shows the +/// code to paste instead. +/// +public static class BrowserSignIn +{ + static readonly TimeSpan Wait = TimeSpan.FromMinutes(5); + + /// For tests: opens the address instead of a browser. Returns whether it could. + internal static Func OpenOverride; + + public static async Task SignInAsync(string url, bool insecure, bool openBrowser, TextReader input) + { + var verifier = Base64Url(RandomNumberGenerator.GetBytes(32)); + var challenge = Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))); + var state = Base64Url(RandomNumberGenerator.GetBytes(16)); + var page = $"{url.TrimEnd('/')}/cli-login?challenge={challenge}&state={state}"; + + string code; + if (openBrowser) + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + var address = $"{page}&port={port}"; + + Console.WriteLine("Opening your browser to sign in. If it doesn't open, go to:"); + Console.WriteLine($" {address}"); + if (!Open(address)) + Console.WriteLine("(Couldn't open a browser here; open the address above on this machine.)"); + Console.WriteLine("Waiting for you to confirm in the browser..."); + + code = await CallbackAsync(listener, state); + } + else + { + Console.WriteLine("Open this address in a browser, sign in, and confirm:"); + Console.WriteLine($" {page}"); + Console.Write("Then paste the code it shows: "); + code = input.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(code)) throw new BitweenApiException("No code was given."); + } + + return await BitweenApi.RedeemAsync(url, code, verifier, insecure); + } + + /// Waits for the browser's redirect back here, and answers it with a page to close. + static async Task CallbackAsync(TcpListener listener, string state) + { + using var timeout = new CancellationTokenSource(Wait); + while (true) + { + TcpClient client; + try + { + client = await listener.AcceptTcpClientAsync(timeout.Token); + } + catch (OperationCanceledException) + { + throw new BitweenApiException($"Nothing came back from the browser within {Wait.TotalMinutes} minutes; run bitween login again."); + } + + using (client) + { + var stream = client.GetStream(); + var line = await ReadRequestLineAsync(stream, timeout.Token); + // "GET /callback?code=...&state=... HTTP/1.1"; anything else (a favicon) gets a 404. + var target = line?.Split(' ') is { Length: >= 2 } parts && parts[0] == "GET" ? parts[1] : null; + if (target == null || !target.StartsWith("/callback", StringComparison.Ordinal)) + { + await RespondAsync(stream, 404, "Not found."); + continue; + } + + var query = HttpUtility.ParseQueryString(target.Contains('?') ? target[(target.IndexOf('?') + 1)..] : ""); + if (query["state"] != state) + { + // Not the sign-in this process started: ignored, still waiting for that one. + await RespondAsync(stream, 400, "This sign-in wasn't started by the bitween CLI waiting here."); + continue; + } + if (!string.IsNullOrEmpty(query["error"])) + { + await RespondAsync(stream, 200, "Sign-in cancelled. You can close this tab."); + throw new BitweenApiException("Sign-in was cancelled in the browser."); + } + + await RespondAsync(stream, 200, "The bitween CLI is signed in. You can close this tab."); + return query["code"] ?? throw new BitweenApiException("The browser sent no code back."); + } + } + } + + static async Task ReadRequestLineAsync(NetworkStream stream, CancellationToken cancellation) + { + var buffer = new byte[8192]; + var read = 0; + while (read < buffer.Length) + { + var n = await stream.ReadAsync(buffer.AsMemory(read), cancellation); + if (n == 0) break; + read += n; + var text = Encoding.ASCII.GetString(buffer, 0, read); + // The whole head, so the browser isn't answered before it has finished asking. + if (text.Contains("\r\n\r\n")) return text[..text.IndexOf("\r\n", StringComparison.Ordinal)]; + } + return null; + } + + static async Task RespondAsync(NetworkStream stream, int status, string message) + { + var body = Encoding.UTF8.GetBytes( + "bitween CLI" + + "" + + $"

{WebUtility.HtmlEncode(message)}

"); + var reason = status switch { 200 => "OK", 400 => "Bad Request", _ => "Not Found" }; + var head = Encoding.ASCII.GetBytes( + $"HTTP/1.1 {status} {reason}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {body.Length}\r\n" + + "Cache-Control: no-store\r\nConnection: close\r\n\r\n"); + await stream.WriteAsync(head); + await stream.WriteAsync(body); + } + + static bool Open(string address) + { + if (OpenOverride != null) return OpenOverride(address); + try + { + var start = OperatingSystem.IsWindows() ? new ProcessStartInfo(address) { UseShellExecute = true } + : OperatingSystem.IsMacOS() ? new ProcessStartInfo("open", [address]) + : new ProcessStartInfo("xdg-open", [address]); + start.RedirectStandardError = !OperatingSystem.IsWindows(); + start.RedirectStandardOutput = !OperatingSystem.IsWindows(); + using var process = Process.Start(start); + return process != null; + } + catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or InvalidOperationException) + { + return false; + } + } + + static string Base64Url(byte[] bytes) => + Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); +} diff --git a/SW.Bitween.Cli/Commands.cs b/SW.Bitween.Cli/Commands.cs new file mode 100644 index 00000000..4521ded4 --- /dev/null +++ b/SW.Bitween.Cli/Commands.cs @@ -0,0 +1,576 @@ +using System.IO.Compression; +using System.Text.Json; +using System.Text.Json.Nodes; +using CommandLine; +using SW.Bitween.Adapters.Tooling; +using SW.Serverless.Contract.Catalog; +using SW.Serverless.Installer; +using SW.Serverless.Tooling; +using SW.Serverless.Tooling.Building; +using SW.Serverless.Tooling.Conformance; +using SW.Serverless.Tooling.Scaffolding; + +namespace SW.Bitween.Cli; + +// ---------------------------------------------------------------------------- signing in + +[Verb("login", HelpText = "Sign in to a Bitween; the other commands then work against it.")] +public class LoginOptions +{ + [Value(0, Required = true, MetaName = "url", HelpText = "The Bitween's address, e.g. https://bitween.example.com")] + public string Url { get; set; } + + [Option('e', "email", HelpText = "Sign in with this email and a password instead of through the browser.")] + public string Email { get; set; } + + [Option("password-stdin", HelpText = "Sign in with an email and a password, read from standard input: for scripts and CI.")] + public bool PasswordStdin { get; set; } + + [Option("no-browser", HelpText = "Sign in through a browser on another machine: show the address, and paste back the code it shows.")] + public bool NoBrowser { get; set; } + + [Option("profile", HelpText = "A name for this Bitween, to keep several; the address's host unless given.")] + public string Profile { get; set; } + + [Option("insecure", HelpText = "Don't check its certificate: for a Bitween on a self-signed development certificate only.")] + public bool Insecure { get; set; } +} + +[Verb("logout", HelpText = "Forget a Bitween signed in to: the current one, or --profile.")] +public class LogoutOptions +{ + [Option("profile", HelpText = "Which Bitween to forget.")] + public string Profile { get; set; } +} + +[Verb("whoami", HelpText = "Which Bitween you're signed in to, and as whom.")] +public class WhoAmIOptions +{ + [Option("profile", HelpText = "Ask about this one rather than the current one.")] + public string Profile { get; set; } +} + +// ---------------------------------------------------------------------------- adapters + +[Verb("init", HelpText = "Write a new Bitween adapter of one kind, ready to build.")] +public class InitOptions +{ + [Value(0, Required = true, MetaName = "name", HelpText = "Its name, e.g. AcmeOrders: its folder, project and class.")] + public string Name { get; set; } + + [Option("kind", Default = "handler", HelpText = "handler, mapper, validator or receiver.")] + public string Kind { get; set; } + + [Option("lang", Default = "dotnet", HelpText = "dotnet, python, node (JavaScript) or typescript.")] + public string Language { get; set; } + + [Option("id", HelpText = "Its id; from the name when not given (AcmeOrders -> acme.orders).")] + public string Id { get; set; } + + [Option("dir", Default = ".", HelpText = "The folder to make it in.")] + public string Directory { get; set; } +} + +[Verb("build", HelpText = "Build an adapter into a package, with its manifest and source.")] +public class BuildOptions +{ + [Value(0, MetaName = "project", Default = ".", HelpText = "The adapter's folder, with its adapter.json.")] + public string Project { get; set; } + + [Option('o', "out", HelpText = "Where the package goes; bin/serverless in the project unless set.")] + public string Output { get; set; } + + [Option("no-source", HelpText = "Leave the source out of the package.")] + public bool NoSource { get; set; } + + [Option("allow", HelpText = "Source files whose secret-scan findings are false positives, separated by spaces.")] + public IEnumerable Allow { get; set; } + + [Option("dry-run", HelpText = "Show the source that would be carried, and build nothing.")] + public bool DryRun { get; set; } +} + +[Verb("test", HelpText = "Start an adapter as Bitween would and check it against the Bitween contract.")] +public class TestOptions +{ + [Value(0, MetaName = "package", Default = ".", HelpText = "A package zip, a package folder, or a project folder (built first).")] + public string Package { get; set; } + + [Option("settings", HelpText = "A JSON file of settings to run it with: { \"Name\": \"value\" }.")] + public string Settings { get; set; } + + [Option("allow-delete", HelpText = "Let a receiver's DeleteFile run: it removes or moves a real file at the source.")] + public bool AllowDelete { get; set; } + + [Option("timeout", Default = 60, HelpText = "Seconds one call may take.")] + public int Timeout { get; set; } +} + +[Verb("run", HelpText = "Start an adapter and call one command.")] +public class RunOptions +{ + [Value(0, MetaName = "package", Default = ".", HelpText = "A package zip, a package folder, or a project folder (built first).")] + public string Package { get; set; } + + [Option("settings", HelpText = "A JSON file of settings to run it with.")] + public string Settings { get; set; } + + [Option("call", Required = true, HelpText = "The command, e.g. Handle.")] + public string Command { get; set; } + + [Option("input", HelpText = "Its argument: JSON, plain text, or @file.")] + public string Input { get; set; } + + [Option("timeout", Default = 60, HelpText = "Seconds the call may take.")] + public int Timeout { get; set; } +} + +/// +/// Where publish, promote, versions and withdraw act: the signed-in Bitween, or - given -p or the +/// SWSL_PROVIDER variable - the storage Bitween reads adapters from, as sw-serverless does it. +/// +public class TargetOptions +{ + [Option("profile", HelpText = "Which signed-in Bitween; the current one unless given.")] + public string Profile { get; set; } + + [Option('p', "provider", HelpText = "Publish straight to storage instead: s3, as (Azure), oc (Oracle), gc (Google) or local.")] + public string Provider { get; set; } + + [Option('a', "accesskey", HelpText = "With -p: the storage access key (or SWSL_ACCESS_KEY).")] + public string AccessKeyId { get; set; } + + [Option('s', "secret", HelpText = "With -p: the storage secret (or SWSL_SECRET_KEY).")] + public string SecretAccessKey { get; set; } + + [Option('b', "bucketname", HelpText = "With -p: the bucket (or SWSL_BUCKET).")] + public string BucketName { get; set; } + + [Option('u', "url", HelpText = "With -p: the storage service URL (or SWSL_SERVICE_URL).")] + public string ServiceUrl { get; set; } + + [Option('c', "cloudfilesconfigpath", HelpText = "With -p: a JSON file with a CloudFiles section.")] + public string CloudFilesConfigPath { get; set; } + + internal bool ToStorage => !string.IsNullOrWhiteSpace(Provider ?? Environment.GetEnvironmentVariable(StorageResolver.Env.Provider)) && Profile == null; + + internal ServerlessUploadOptions ToUploadOptions() => new StorageFlags + { + Provider = Provider, AccessKeyId = AccessKeyId, SecretAccessKey = SecretAccessKey, + BucketName = BucketName, ServiceUrl = ServiceUrl, CloudFilesConfigPath = CloudFilesConfigPath, + }.Resolve(); +} + +[Verb("publish", HelpText = "Publish a package: through the signed-in Bitween, or straight to storage with -p and its flags.")] +public class PublishOptions : TargetOptions +{ + [Value(0, Required = true, MetaName = "package", HelpText = "The package zip bitween adapter build made.")] + public string Package { get; set; } + + [Option('v', "version", HelpText = "major, minor, patch or an exact version; the package's own version unless given.")] + public string Version { get; set; } + + [Option("current", HelpText = "Make it the version that runs where nothing pins a version. Not without it.")] + public bool Current { get; set; } + + [Option("notes", HelpText = "Release notes, instead of the package's own.")] + public string Notes { get; set; } +} + +[Verb("promote", HelpText = "Make a published version the one that runs where nothing pins a version.")] +public class PromoteOptions : TargetOptions +{ + [Value(0, Required = true, MetaName = "adapter-id")] public string AdapterId { get; set; } + [Value(1, Required = true, MetaName = "version")] public string Version { get; set; } +} + +[Verb("versions", HelpText = "List an adapter's published versions.")] +public class VersionsOptions : TargetOptions +{ + [Value(0, Required = true, MetaName = "adapter-id")] public string AdapterId { get; set; } +} + +[Verb("withdraw", HelpText = "Take a published version out of use: still listed, never offered for pinning or made current.")] +public class WithdrawOptions : TargetOptions +{ + [Value(0, Required = true, MetaName = "adapter-id")] public string AdapterId { get; set; } + [Value(1, Required = true, MetaName = "version")] public string Version { get; set; } +} + +/// What each command does. +public static partial class Commands +{ + public const int Success = 0; + public const int Failure = 1; + + // ------------------------------------------------------------------ signing in + + public static async Task Login(LoginOptions opts, Profiles profiles, TextReader input, Func readSecret) + { + if (!Uri.TryCreate(opts.Url, UriKind.Absolute, out var url) || (url.Scheme != "https" && url.Scheme != "http")) + { + Console.WriteLine($"'{opts.Url}' isn't an address: give one like https://bitween.example.com"); + return Failure; + } + var name = string.IsNullOrWhiteSpace(opts.Profile) ? url.Host : opts.Profile; + var address = url.GetLeftPart(UriPartial.Path); + + // Through the browser unless an email or a password was given: it works for every account, + // whichever way it signs in, and the password never passes through the terminal. + if (string.IsNullOrWhiteSpace(opts.Email) && !opts.PasswordStdin) + { + if (url.Scheme == "http" && !url.IsLoopback) + Console.WriteLine("Warning: signing in over http sends your session unencrypted; use https."); + var signedIn = await BrowserSignIn.SignInAsync(address, opts.Insecure, openBrowser: !opts.NoBrowser, input); + profiles.Put(name, signedIn, makeCurrent: true); + Console.WriteLine($"Signed in to {signedIn.Url} as {signedIn.Email} (profile {name})."); + return Success; + } + + if (url.Scheme == "http" && !url.IsLoopback) + Console.WriteLine("Warning: signing in over http sends your password unencrypted; use https."); + + var email = opts.Email; + if (string.IsNullOrWhiteSpace(email)) + { + Console.Write("Email: "); + email = input.ReadLine()?.Trim(); + } + string password; + if (opts.PasswordStdin) password = input.ReadLine(); + else + { + Console.Write("Password: "); + password = readSecret(); + Console.WriteLine(); + } + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) + { + Console.WriteLine("An email and a password are needed."); + return Failure; + } + + var profile = await BitweenApi.SignInAsync(address, email, password, opts.Insecure); + profiles.Put(name, profile, makeCurrent: true); + Console.WriteLine($"Signed in to {profile.Url} as {email} (profile {name})."); + return Success; + } + + public static async Task Logout(LogoutOptions opts, Profiles profiles) + { + var (name, profile) = profiles.Find(opts.Profile); + if (profile == null) + { + Console.WriteLine(name == null ? "You aren't signed in to any Bitween." : $"There's no profile named {name}."); + return Failure; + } + var ended = await BitweenApi.EndSessionAsync(profile); + profiles.Remove(name); + Console.WriteLine(ended + ? $"Signed out of {profile.Url} (profile {name})." + : $"Forgot {profile.Url} here (profile {name}), but couldn't reach it to end the session there; it ends after 30 days unused."); + return Success; + } + + public static async Task WhoAmI(WhoAmIOptions opts, Profiles profiles) + { + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + var me = await api.GetAsync("api/accounts/profile"); + Console.WriteLine(api.AsService + ? $"{(string)me?["name"]} (service account, from BITWEEN_TOKEN) on {api.Url}" + : $"{(string)me?["name"]} <{(string)me?["email"]}> on {api.Url}"); + var permissions = me?["permissions"]?.AsArray().Select(p => (string)p).Where(p => p.StartsWith("adapter-source.")).ToList() ?? []; + Console.WriteLine(permissions.Count == 0 + ? "No adapter permissions: you can build and test, not publish." + : $"Adapter permissions: {string.Join(", ", permissions)}"); + return Success; + } + + /// A service token to sign in with instead of a profile: BITWEEN_TOKEN, or the file BITWEEN_TOKEN_FILE names. + internal static string ServiceToken() + { + var token = Environment.GetEnvironmentVariable("BITWEEN_TOKEN"); + var file = Environment.GetEnvironmentVariable("BITWEEN_TOKEN_FILE"); + if (string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(file) && File.Exists(file)) token = File.ReadAllText(file); + return string.IsNullOrWhiteSpace(token) ? null : token.Trim(); + } + + /// + /// The Bitween to act on: the profile asked for; else, with a service token in the environment, as + /// that service account at BITWEEN_URL; else the current profile. + /// + static BitweenApi Api(Profiles profiles, string profileName) + { + if (profileName == null && ServiceToken() is { } token) + { + var url = Environment.GetEnvironmentVariable("BITWEEN_URL"); + if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _)) + { + Console.WriteLine("BITWEEN_TOKEN is set but BITWEEN_URL isn't: set it to the Bitween's address."); + return null; + } + var insecure = Environment.GetEnvironmentVariable("BITWEEN_INSECURE") is "1" or "true"; + return new BitweenApi(url, token, insecure); + } + + var (name, profile) = profiles.Find(profileName); + if (profile == null) + { + Console.WriteLine(name == null || profileName == null + ? "You aren't signed in to a Bitween: run bitween login first, or give storage flags (-p ...)." + : $"There's no profile named {name}: bitween login --profile {name}."); + return null; + } + return new BitweenApi(profiles, name, profile); + } + + // ------------------------------------------------------------------ adapters, locally + + public static int Init(InitOptions opts) + { + var result = BitweenAdapters.Scaffold(new ScaffoldRequest + { + Name = opts.Name, Id = opts.Id, Language = opts.Language, Kind = opts.Kind, ParentDirectory = opts.Directory, + }); + if (!result.Succeeded) + { + foreach (var problem in result.Problems) Console.WriteLine(problem); + return Failure; + } + Console.WriteLine($"Made a Bitween {opts.Kind} in {result.ProjectDirectory}:"); + foreach (var file in result.Files) Console.WriteLine($" {file}"); + Console.WriteLine("Next: bitween adapter build, then bitween adapter test --settings settings.json"); + return Success; + } + + public static async Task Build(BuildOptions opts) + { + var request = BitweenAdapters.BuildRequest(opts.Project, opts.Output, Console.WriteLine); + request.IncludeSource = !opts.NoSource; + request.AllowedFiles = new HashSet(opts.Allow ?? [], StringComparer.OrdinalIgnoreCase); + request.DryRun = opts.DryRun; + var result = await PackageBuilder.BuildAsync(request); + + if (result.SourceFiles.Count > 0) + { + Console.WriteLine($"Source carried ({result.SourceFiles.Count} files, {result.SourceFiles.Sum(f => f.Bytes) / 1024} KB):"); + foreach (var (path, bytes) in result.SourceFiles.OrderBy(f => f.Path, StringComparer.Ordinal)) + Console.WriteLine($" {path} ({bytes} B)"); + } + foreach (var warning in result.Warnings) Console.WriteLine($"Warning: {warning}"); + foreach (var problem in result.Problems) Console.WriteLine(problem); + if (!result.Succeeded) return Failure; + if (!opts.DryRun) Console.WriteLine($"Built {result.ZipPath}"); + return Success; + } + + public static async Task Test(TestOptions opts) + { + var (package, cleanup) = await PackageFolderAsync(opts.Package); + if (package == null) return Failure; + try + { + var report = await new ConformanceRunner().RunAsync(new ConformanceOptions + { + PackageDirectory = package, + Settings = ReadSettings(opts.Settings), + AllowDelete = opts.AllowDelete, + Contracts = { BitweenAdapters.Contract }, + CommandTimeoutSeconds = opts.Timeout, + Log = Console.WriteLine, + }); + foreach (var check in report.Checks) + Console.WriteLine($"{Mark(check.Outcome)} {check.Name}{(string.IsNullOrEmpty(check.Detail) ? "" : " - " + check.Detail)}"); + var failed = report.Checks.Count(c => c.Outcome == CheckOutcome.Failed); + Console.WriteLine(failed == 0 ? "Conforms." : $"{failed} check{(failed == 1 ? "" : "s")} failed."); + return report.Passed ? Success : Failure; + } + finally + { + cleanup(); + } + } + + public static async Task Run(RunOptions opts) + { + var (package, cleanup) = await PackageFolderAsync(opts.Package); + if (package == null) return Failure; + try + { + await using var host = await LocalAdapterHost.StartAsync(package, ReadSettings(opts.Settings), commandTimeoutSeconds: opts.Timeout); + Console.WriteLine(await host.CallAsync(opts.Command, ReadInput(opts.Input))); + return Success; + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + Console.WriteLine(ex.GetBaseException().Message); + return Failure; + } + finally + { + cleanup(); + } + } + + // ------------------------------------------------------------------ adapters, published + + public static async Task Publish(PublishOptions opts, Profiles profiles) + { + if (!File.Exists(opts.Package)) + { + Console.WriteLine($"There is no package at {opts.Package}; bitween adapter build makes one."); + return Failure; + } + + if (opts.ToStorage) + { + var result = await PackagePublisher.PublishPackageAsync(CloudFilesFactory.Create(opts.ToUploadOptions()), new PublishPackageRequest + { + PackagePath = opts.Package, + Version = opts.Version, + Promote = opts.Current, + ReleaseNotes = opts.Notes, + PublishedBy = StorageResolver.ResolvePublishedBy(null), + }, Console.WriteLine); + Console.WriteLine($"Published {result.Manifest.Id} {result.Version}{(opts.Current ? ", current" : "")}."); + return Success; + } + + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + var query = new List(); + if (!string.IsNullOrWhiteSpace(opts.Version)) query.Add("version=" + Uri.EscapeDataString(opts.Version)); + if (opts.Current) query.Add("current=true"); + if (!string.IsNullOrWhiteSpace(opts.Notes)) query.Add("releaseNotes=" + Uri.EscapeDataString(opts.Notes)); + var published = await api.PostFileAsync("api/adapters/packages" + (query.Count > 0 ? "?" + string.Join("&", query) : ""), opts.Package, "application/zip"); + Console.WriteLine($"Published {(string)published?["adapterId"]} {(string)published?["version"]} to {api.Url}" + + ((bool?)published?["current"] == true ? ", current." : ". It isn't current: bitween adapter promote makes it so.")); + return Success; + } + + public static async Task Promote(PromoteOptions opts, Profiles profiles) + { + if (opts.ToStorage) + { + var work = Path.Combine(Path.GetTempPath(), "bitween-cli", Guid.NewGuid().ToString("N")); + try { await new AdapterRepository(CloudFilesFactory.Create(opts.ToUploadOptions()), Console.WriteLine).PromoteAsync(opts.AdapterId, opts.Version, work); } + finally { try { Directory.Delete(work, true); } catch { } } + return Success; + } + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + await api.PostAsync("api/adapters/promote", new { adapterId = opts.AdapterId, version = opts.Version }); + Console.WriteLine($"{opts.AdapterId} {opts.Version} is now current on {api.Url}."); + return Success; + } + + public static async Task Versions(VersionsOptions opts, Profiles profiles) + { + VersionListing listing; + if (opts.ToStorage) + listing = await new AdapterRepository(CloudFilesFactory.Create(opts.ToUploadOptions()), _ => { }).ListVersionsAsync(opts.AdapterId); + else + { + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + var json = await api.GetAsync("api/adapters/versions?adapterId=" + Uri.EscapeDataString(opts.AdapterId)); + listing = json.Deserialize(new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + if (listing.Versions.Count == 0) + { + Console.WriteLine($"{opts.AdapterId} has no published versions."); + return Success; + } + foreach (var v in listing.Versions) + Console.WriteLine($"{(v.Current ? "*" : " ")} {v.Version,-12} {v.PublishedOn?.ToString("yyyy-MM-dd HH:mm") ?? "",-17} {v.PublishedBy,-20}{(v.Withdrawn ? " withdrawn" : "")}"); + return Success; + } + + public static async Task Withdraw(WithdrawOptions opts, Profiles profiles) + { + if (opts.ToStorage) + { + await new AdapterRepository(CloudFilesFactory.Create(opts.ToUploadOptions()), Console.WriteLine).WithdrawAsync(opts.AdapterId, opts.Version); + return Success; + } + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + await api.PostAsync("api/adapters/withdraw", new { adapterId = opts.AdapterId, version = opts.Version }); + Console.WriteLine($"{opts.AdapterId} {opts.Version} is withdrawn on {api.Url}."); + return Success; + } + + // ------------------------------------------------------------------ helpers + + static string Mark(CheckOutcome outcome) => outcome switch + { + CheckOutcome.Passed => "PASS", + CheckOutcome.Failed => "FAIL", + _ => "SKIP", + }; + + /// A package folder from a zip, a package folder, or a project folder built first. + static async Task<(string Folder, Action Cleanup)> PackageFolderAsync(string given) + { + var path = Path.GetFullPath(given); + if (File.Exists(path) && path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + var folder = Path.Combine(Path.GetTempPath(), "bitween-cli", Guid.NewGuid().ToString("N")); + ZipFile.ExtractToDirectory(path, folder); + return (folder, () => TryDelete(folder)); + } + + if (Directory.Exists(path) && File.Exists(Path.Combine(path, AdapterManifest.FileName)) && IsProject(path)) + { + var output = Path.Combine(Path.GetTempPath(), "bitween-cli", Guid.NewGuid().ToString("N")); + var built = await PackageBuilder.BuildAsync(BitweenAdapters.BuildRequest(path, output, Console.WriteLine)); + foreach (var problem in built.Problems) Console.WriteLine(problem); + return built.Succeeded ? (built.PackageDirectory, () => TryDelete(output)) : (null, () => TryDelete(output)); + } + + if (Directory.Exists(path) && File.Exists(Path.Combine(path, AdapterManifest.FileName))) + return (path, () => { }); + + Console.WriteLine($"{given} is neither a package zip, a package folder nor a project folder"); + return (null, () => { }); + } + + /// A project rather than a built package: a .NET project file, or no sign of a build. + static bool IsProject(string folder) + { + if (Directory.GetFiles(folder, "*.*proj").Length > 0) return true; + try + { + var runtime = AdapterManifest.Parse(File.ReadAllText(Path.Combine(folder, AdapterManifest.FileName))).Runtime; + if (string.Equals(runtime, AdapterManifest.PythonRuntime, StringComparison.OrdinalIgnoreCase)) + return !File.Exists(Path.Combine(folder, PythonBuild.EntryScript)); + if (string.Equals(runtime, AdapterManifest.NodeRuntime, StringComparison.OrdinalIgnoreCase)) + return !File.Exists(Path.Combine(folder, "node_modules", "@simplyworks", "sw-serverless", "package.json")); + } + catch (JsonException) { } + return false; + } + + static void TryDelete(string folder) + { + try { Directory.Delete(folder, true); } catch { } + } + + static IDictionary ReadSettings(string path) + { + if (string.IsNullOrWhiteSpace(path)) return new Dictionary(); + using var document = JsonDocument.Parse(File.ReadAllText(path)); + return document.RootElement.EnumerateObject().ToDictionary(p => p.Name, + p => p.Value.ValueKind == JsonValueKind.String ? p.Value.GetString() : p.Value.GetRawText()); + } + + static object ReadInput(string input) + { + if (input == null) return null; + if (input.StartsWith('@')) input = File.ReadAllText(input[1..]); + try { return Newtonsoft.Json.Linq.JToken.Parse(input); } + catch (Newtonsoft.Json.JsonException) { return input; } + } +} diff --git a/SW.Bitween.Cli/Configuration/ConfigFile.cs b/SW.Bitween.Cli/Configuration/ConfigFile.cs new file mode 100644 index 00000000..84072739 --- /dev/null +++ b/SW.Bitween.Cli/Configuration/ConfigFile.cs @@ -0,0 +1,141 @@ +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace SW.Bitween.Cli.Configuration; + +/// A configuration problem: in the file, or between the file and the Bitween. +public class ConfigException(string message) : Exception(message); + +/// +/// The desired configuration: one file, or every .yaml and .yml in a folder, merged. Each document is +/// +/// apiVersion: bitween/v1 +/// kind: Configuration +/// metadata: { name: acme } +/// informationTypes: [...] +/// partners: [...] +/// +/// Strings may hold ${secret:NAME} (from the variable NAME, or the file NAME in the secrets folder), +/// ${env:NAME} and ${file:relative/path}; secrets are never printed. +/// +public class ConfigFile +{ + public const string ApiVersion = "bitween/v1"; + public const string Kind = "Configuration"; + + /// What this configuration is called: what it remembers it manages is kept under this name. + public string Name { get; private set; } + + public Dictionary> Sections { get; } = new(StringComparer.Ordinal); + + /// Every value that came from a secret, so it can be kept out of what's printed. + public HashSet SecretValues { get; } = new(StringComparer.Ordinal); + + static readonly Regex Reference = new(@"\$\{(secret|env|file):([^}]+)\}", RegexOptions.Compiled); + + public static ConfigFile Load(string path, string secretsDir = null) + { + var files = Directory.Exists(path) + ? Directory.EnumerateFiles(path, "*.*", SearchOption.TopDirectoryOnly) + .Where(f => f.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f, StringComparer.Ordinal).ToList() + : File.Exists(path) ? [path] : throw new ConfigException($"There's no file or folder at {path}."); + if (files.Count == 0) throw new ConfigException($"There are no .yaml files in {path}."); + + var config = new ConfigFile(); + var missing = new SortedSet(StringComparer.Ordinal); + foreach (var file in files) + { + List documents; + try { documents = Yaml.Read(File.ReadAllText(file)); } + catch (YamlDotNet.Core.YamlException ex) { throw new ConfigException($"{file}: {ex.Message}"); } + + foreach (var document in documents.Where(d => d != null)) + { + if (document is not JsonObject root) throw new ConfigException($"{file}: expected a mapping at the top."); + config.Add(file, root, Path.GetDirectoryName(Path.GetFullPath(file)), secretsDir, missing); + } + } + + if (missing.Count > 0) + throw new ConfigException("These aren't set: " + string.Join(", ", missing) + + ". Set each as a variable, or put it in a file of that name in the secrets folder (--secrets-dir)."); + if (config.Name == null) throw new ConfigException("No metadata.name: name the configuration, e.g. metadata: { name: acme }."); + return config; + } + + void Add(string file, JsonObject root, string folder, string secretsDir, ISet missing) + { + var version = (string)root["apiVersion"]; + var kind = (string)root["kind"]; + if (version != ApiVersion) throw new ConfigException($"{file}: apiVersion must be {ApiVersion}, not {version ?? "missing"}."); + if (kind != Kind) throw new ConfigException($"{file}: kind must be {Kind}, not {kind ?? "missing"}."); + + var name = (string)root["metadata"]?["name"]; + if (name != null) + { + if (Name != null && Name != name) throw new ConfigException($"{file}: named {name}, but another file is named {Name}. One configuration, one name."); + Name = name; + } + + foreach (var (section, value) in root) + { + if (section is "apiVersion" or "kind" or "metadata") continue; + if (!Kinds.Sections.Contains(section)) + throw new ConfigException($"{file}: there's no section {section}. Known: {string.Join(", ", Kinds.Sections)}."); + if (value is null) continue; + if (value is not JsonArray items) throw new ConfigException($"{file}: {section} must be a list."); + + var list = Sections.TryGetValue(section, out var existing) ? existing : Sections[section] = []; + foreach (var item in items) + { + if (item is not JsonObject obj) throw new ConfigException($"{file}: every item in {section} must be a mapping."); + var resolved = (JsonObject)Resolve(obj.DeepClone(), folder, secretsDir, missing); + // A package is found next to the file that names it, wherever apply runs from. + if (section == Kinds.Adapters && (string)resolved["package"] is { Length: > 0 } package) + resolved["package"] = Path.GetFullPath(Path.Combine(folder, package)); + list.Add(resolved); + } + } + } + + JsonNode Resolve(JsonNode node, string folder, string secretsDir, ISet missing) + { + switch (node) + { + case JsonObject obj: + foreach (var key in obj.Select(p => p.Key).ToList()) obj[key] = Resolve(obj[key]?.DeepClone(), folder, secretsDir, missing); + return obj; + case JsonArray list: + return new JsonArray(list.Select(i => Resolve(i?.DeepClone(), folder, secretsDir, missing)).ToArray()); + case JsonValue v when v.TryGetValue(out var s) && s.Contains("${"): + var secret = false; + var resolved = Reference.Replace(s, m => + { + var (source, name) = (m.Groups[1].Value, m.Groups[2].Value.Trim()); + switch (source) + { + case "secret": + secret = true; + var value = Environment.GetEnvironmentVariable(name); + var inFolder = secretsDir == null ? null : Path.Combine(secretsDir, name); + if (value == null && inFolder != null && File.Exists(inFolder)) value = File.ReadAllText(inFolder).TrimEnd('\r', '\n'); + if (value == null) missing.Add(name); + return value ?? ""; + case "env": + var env = Environment.GetEnvironmentVariable(name); + if (env == null) missing.Add(name); + return env ?? ""; + default: + var full = Path.GetFullPath(Path.Combine(folder, name)); + if (!File.Exists(full)) throw new ConfigException($"${{file:{name}}}: there's no file at {full}."); + return File.ReadAllText(full); + } + }); + if (secret && resolved.Length > 0) SecretValues.Add(resolved); + return JsonValue.Create(resolved); + default: + return node; + } + } +} diff --git a/SW.Bitween.Cli/Configuration/Exporter.cs b/SW.Bitween.Cli/Configuration/Exporter.cs new file mode 100644 index 00000000..f38c85d6 --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Exporter.cs @@ -0,0 +1,77 @@ +using System.Text; +using System.Text.Json.Nodes; + +namespace SW.Bitween.Cli.Configuration; + +/// +/// A Bitween's configuration as a file apply takes: names for ids, nothing that only says a default, +/// and each secret Bitween won't show as a ${secret:NAME} to be supplied when it's applied. +/// +public static class Exporter +{ + public static async Task<(string Yaml, List Secrets, int Count)> ExportAsync(Context ctx, string name, ICollection only = null) + { + await new Planner(ctx, null).ReadAsync(); + + var root = new JsonObject + { + ["apiVersion"] = ConfigFile.ApiVersion, + ["kind"] = ConfigFile.Kind, + ["metadata"] = new JsonObject { ["name"] = name }, + }; + var secrets = new List(); + var count = 0; + foreach (var kind in Kinds.All.Where(k => only == null || only.Count == 0 || only.Contains(k.Section))) + { + var items = ctx.Current[kind.Section].Where(e => e.Key != null).OrderBy(e => e.Key, StringComparer.Ordinal).ToList(); + if (items.Count == 0) continue; + root[kind.Section] = new JsonArray(items.Select(e => + { + var file = Values.Tidy((JsonObject)e.File.DeepClone()); + if (kind is Partners && file["keys"] is JsonObject keys) + foreach (var key in keys.Select(k => k.Key).ToList()) + keys[key] = Secret(secrets, kind, e.Key, "key", key); + return (JsonNode)Unmask(file, secrets, kind, e.Key, []); + }).ToArray()); + count += items.Count; + } + return (Yaml.Write(root), secrets, count); + } + + static JsonNode Unmask(JsonNode node, List secrets, Kind kind, string key, List path) + { + switch (node) + { + case JsonObject obj: + foreach (var name in obj.Select(p => p.Key).ToList()) + obj[name] = Unmask(obj[name]?.DeepClone(), secrets, kind, key, [.. path, name]); + return obj; + case JsonArray list: + return new JsonArray(list.Select((x, i) => Unmask(x?.DeepClone(), secrets, kind, key, [.. path, (i + 1).ToString()])).ToArray()); + default: + return Values.IsMasked(node) ? Secret(secrets, kind, key, path.Where(p => p != "properties").ToArray()) : node; + } + } + + static JsonNode Secret(List secrets, Kind kind, string key, params string[] path) + { + var name = Name([kind.Noun, key, .. path]); + var unique = name; + for (var n = 2; secrets.Contains(unique); n++) unique = $"{name}_{n}"; + secrets.Add(unique); + return $"${{secret:{unique}}}"; + } + + /// "partner", "Acme Ltd", "key", "main" as PARTNER_ACME_LTD_KEY_MAIN. + public static string Name(IEnumerable parts) + { + var text = new StringBuilder(); + foreach (var c in string.Join("_", parts).ToUpperInvariant()) + { + var keep = char.IsAsciiLetterOrDigit(c) ? c : '_'; + if (keep == '_' && (text.Length == 0 || text[^1] == '_')) continue; + text.Append(keep); + } + return text.ToString().TrimEnd('_'); + } +} diff --git a/SW.Bitween.Cli/Configuration/Kind.cs b/SW.Bitween.Cli/Configuration/Kind.cs new file mode 100644 index 00000000..d154981d --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Kind.cs @@ -0,0 +1,292 @@ +using System.Text.Json.Nodes; + +namespace SW.Bitween.Cli.Configuration; + +/// Something in Bitween as it is now: its id, its key in the file, and how the file would say it. +public record Existing(string Id, string Key, JsonObject File, JsonObject Api); + +/// What the commands share: the API, and every item's id by key, which grows as apply creates them. +public class Context(BitweenApi api, ISet secrets = null) +{ + public BitweenApi Api { get; } = api; + public ISet Secrets { get; } = secrets ?? new HashSet(); + public Dictionary> Current { get; } = new(); + + readonly Dictionary> ids = new(); + readonly Dictionary> keys = new(); + + readonly Dictionary> duplicates = new(); + + public void Know(string section, string key, string id) + { + if (key == null) return; + if (Map(ids, section).TryGetValue(key, out var other) && other != id) + (duplicates.TryGetValue(section, out var set) ? set : duplicates[section] = new(StringComparer.Ordinal)).Add(key); + Map(ids, section)[key] = id; + Map(keys, section)[id] = key; + } + + /// Whether Bitween has more than one by this name: the file can't say which it means. + public bool Ambiguous(string section, string key) => duplicates.TryGetValue(section, out var set) && set.Contains(key); + + public void Forget(string section, string key) + { + if (Map(ids, section).Remove(key, out var id)) Map(keys, section).Remove(id); + } + + public bool Has(string section, string key) => key != null && Map(ids, section).ContainsKey(key); + + /// The id to send for a name in the file. + public string IdOf(string section, string key) => + key == null ? null + : Ambiguous(section, key) ? throw new ConfigException($"There's more than one {Kinds.Of(section).Noun} named {Quote(key)}; rename all but one.") + : Map(ids, section).TryGetValue(key, out var id) ? id + : throw new ConfigException($"There's no {Kinds.Of(section).Noun} {Quote(key)}."); + + /// The name to write for an id the API answered with; null when it isn't one this knows. + public string KeyOf(string section, JsonNode id) => + id == null ? null : Map(keys, section).GetValueOrDefault(id.ToString()); + + static Dictionary Map(Dictionary> all, string section) => + all.TryGetValue(section, out var map) ? map : all[section] = new(StringComparer.Ordinal); + + public static string Quote(string key) => $"\"{key}\""; +} + +public enum FieldMode +{ + /// The same in the file as in the API; nested values compare by what the file says. + Plain, + + /// An id in the API, the other item's name in the file. + Ref, + + /// [{key, value}] in the API, a mapping in the file; replaces all keys. + KeyValues, + + /// A mapping in both, of strings; replaces all keys. + Dictionary, + + /// [{id}] in the API, a list of names in the file; compared as a set. + RefList, + + /// A list of strings whose order means nothing. + Set, +} + +/// One field: what the file calls it, what the API calls it, and how one becomes the other. +public record Field(string File, string Api, FieldMode Mode = FieldMode.Plain, string Ref = null) +{ + public static Field Same(string name) => new(name, name); + public static Field Reference(string file, string api, string section) => new(file, api, FieldMode.Ref, section); +} + +/// +/// One section of the file and the resource it configures. The defaults fit most: list, read each, +/// create, update by sending everything back with the file's fields over it, delete. A resource that +/// works differently says so by overriding. +/// +public abstract class Kind +{ + /// The section in the file, e.g. informationTypes. + public abstract string Section { get; } + + /// What one is called in sentences, e.g. information type. + public abstract string Noun { get; } + + /// Its path under /api, e.g. documents. + public abstract string Resource { get; } + + /// The field in the file that names it, unique among its kind. + public virtual string KeyField => "name"; + + public abstract IReadOnlyList Fields { get; } + + /// Fields Bitween won't change once the item exists; a different value is an error, not an update. + public virtual IReadOnlyList Fixed => []; + + /// Sections whose items this one's fields name, so those are applied first. + public virtual IEnumerable DependsOn => Fields.Where(f => f.Ref != null).Select(f => f.Ref).Distinct(); + + /// Keys of others of its own kind this one names, which have to exist first. + public IEnumerable NamesOwnKind(JsonObject want) => + Fields.Where(f => f.Ref == Section && want.ContainsKey(f.File)).Select(f => want[f.File]?.ToString()).Where(k => !string.IsNullOrEmpty(k)); + + public virtual string KeyOf(JsonObject file) => (string)file[KeyField]; + + // ------------------------------------------------------------------ reading + + /// + /// Every one there is, in the API's form. Turned into the file's form only once every section has + /// been read, since names for ids can point anywhere, a subscription to another one included. + /// + public virtual async Task> ReadAsync(Context ctx) + { + var rows = await ListAsync(ctx); + var full = await Parallel(rows, row => DetailOf(ctx, row)); + return full.Where(x => x.Api != null && Include(x.Id, x.Api)).ToList(); + } + + /// Its key, from the API's form: what the file calls it. + public virtual string KeyOfApi(Context ctx, JsonObject api) => + api[Fields.First(f => f.File == KeyField).Api]?.ToString(); + + /// Its rows from the list endpoint. + protected virtual async Task> ListAsync(Context ctx) => await Searchy(ctx, Resource); + + /// One row in full: the GET by id, unless the list row already is. + protected virtual async Task<(string Id, JsonObject Api)> DetailOf(Context ctx, JsonObject row) + { + var id = row["id"]!.ToString(); + return (id, await ctx.Api.GetAsync($"api/{Resource}/{Uri.EscapeDataString(id)}") as JsonObject); + } + + /// False for what isn't configuration to manage, such as built-in items. + protected virtual bool Include(string id, JsonObject api) => true; + + public Existing ToExisting(Context ctx, string id, JsonObject api) + { + var file = ToFile(ctx, api); + return new Existing(id, KeyOf(file), file, api); + } + + /// The API's form as the file would write it. Names for ids are looked up, so read what's referred to first. + public virtual JsonObject ToFile(Context ctx, JsonObject api) + { + var file = new JsonObject(); + foreach (var f in Fields) + { + var value = api[f.Api]; + file[f.File] = f.Mode switch + { + FieldMode.Ref => ctx.KeyOf(f.Ref, value), + FieldMode.KeyValues => Values.ToMap(value), + FieldMode.RefList => new JsonArray((value as JsonArray ?? []) + .Select(x => ctx.KeyOf(f.Ref, x is JsonObject o ? o["id"] : x)).Where(k => k != null) + .OrderBy(k => k, StringComparer.Ordinal).Select(k => (JsonNode)k).ToArray()), + _ => value?.DeepClone(), + }; + } + return file; + } + + // ------------------------------------------------------------------ comparing + + /// The fields the file sets that differ from what Bitween has. + public virtual List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = new List(); + foreach (var f in Fields) + { + if (!want.ContainsKey(f.File) || f.File == KeyField) continue; + var same = f.Mode switch + { + FieldMode.KeyValues or FieldMode.Dictionary => Values.Same(want[f.File], have.File[f.File], exactKeys: true), + FieldMode.RefList or FieldMode.Set => SameSet(want[f.File], have.File[f.File]), + _ => Values.Same(want[f.File], have.File[f.File]), + }; + if (!same) differ.Add(f.File); + } + return differ; + } + + protected static bool SameSet(JsonNode want, JsonNode have) => + (want as JsonArray ?? []).Select(x => x?.ToString()).Order(StringComparer.Ordinal) + .SequenceEqual((have as JsonArray ?? []).Select(x => x?.ToString()).Order(StringComparer.Ordinal)); + + /// Names in the file's fields that are neither in Bitween nor in the file. + public virtual IEnumerable Unresolved(JsonObject want, Func known) + { + foreach (var f in Fields.Where(f => f.Ref != null && want.ContainsKey(f.File))) + { + var names = f.Mode == FieldMode.RefList + ? (want[f.File] as JsonArray ?? []).Select(x => x?.ToString()) + : [want[f.File]?.ToString()]; + foreach (var name in names.Where(n => !string.IsNullOrEmpty(n) && !known(f.Ref, n))) + yield return $"{f.File}: there's no {Kinds.Of(f.Ref).Noun} {Context.Quote(name)}"; + } + } + + // ------------------------------------------------------------------ writing + + /// The file's fields in the API's form, ids for names. + public virtual JsonObject ToApi(Context ctx, JsonObject want) + { + var api = new JsonObject(); + foreach (var f in Fields.Where(f => want.ContainsKey(f.File))) + { + var value = want[f.File]; + api[f.Api] = f.Mode switch + { + FieldMode.Ref => Id(ctx.IdOf(f.Ref, value?.ToString())), + FieldMode.KeyValues => Values.ToKeyValues(value), + FieldMode.Dictionary => Values.ToStrings(value), + FieldMode.RefList => new JsonArray((value as JsonArray ?? []) + .Select(x => (JsonNode)new JsonObject { ["id"] = Id(ctx.IdOf(f.Ref, x?.ToString())) }).ToArray()), + _ => value?.DeepClone(), + }; + } + return api; + } + + /// Ids go back as numbers where they are numbers. + protected static JsonNode Id(string id) => id == null ? null : int.TryParse(id, out var n) ? JsonValue.Create(n) : JsonValue.Create(id); + + /// Creates it and returns its id. + public virtual async Task CreateAsync(Context ctx, JsonObject want) + { + var answer = await ctx.Api.PostAsync($"api/{Resource}", CreateBody(ctx, want)); + return IdIn(answer); + } + + protected virtual JsonObject CreateBody(Context ctx, JsonObject want) => ToApi(ctx, want); + + /// An id however the API answered it: a number, or { id }. + protected static string IdIn(JsonNode answer) => answer is JsonObject o ? o["id"]?.ToString() : answer?.ToString(); + + public virtual async Task UpdateAsync(Context ctx, Existing have, JsonObject want) => + await ctx.Api.PostAsync($"api/{Resource}/{Uri.EscapeDataString(have.Id)}", UpdateBody(ctx, have, want)); + + /// Everything Bitween has, with the file's fields over it: updates replace the whole item. + protected virtual JsonObject UpdateBody(Context ctx, Existing have, JsonObject want) + { + var body = (JsonObject)have.Api.DeepClone(); + foreach (var (key, value) in ToApi(ctx, want)) body[key] = value?.DeepClone(); + return body; + } + + public virtual async Task DeleteAsync(Context ctx, Existing have) => + await ctx.Api.DeleteAsync($"api/{Resource}/{Uri.EscapeDataString(have.Id)}"); + + // ------------------------------------------------------------------ helpers + + /// Every row of a Searchy list, a page at a time. + protected static async Task> Searchy(Context ctx, string resource, string filter = null) + { + const int size = 500; + var rows = new List(); + for (var page = 0; ; page++) + { + var answer = await ctx.Api.GetAsync($"api/{resource}?page={page}&size={size}" + (filter == null ? "" : "&filter=" + filter)); + var result = answer?["result"] as JsonArray ?? []; + rows.AddRange(result.OfType()); + var total = (int?)answer?["totalCount"] ?? rows.Count; + if (result.Count < size || rows.Count >= total) return rows; + } + } + + /// The rows of a plain list that answers { result } to a large limit. + protected static async Task> Listed(Context ctx, string path) => + ((await ctx.Api.GetAsync(path))?["result"] as JsonArray ?? []).OfType().ToList(); + + protected static async Task> Parallel(IEnumerable items, Func> each) + { + using var gate = new SemaphoreSlim(8); + return (await Task.WhenAll(items.Select(async item => + { + await gate.WaitAsync(); + try { return await each(item); } + finally { gate.Release(); } + }))).ToList(); + } +} diff --git a/SW.Bitween.Cli/Configuration/Kinds.cs b/SW.Bitween.Cli/Configuration/Kinds.cs new file mode 100644 index 00000000..dd765e83 --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Kinds.cs @@ -0,0 +1,646 @@ +using System.Text.Json.Nodes; +using static SW.Bitween.Cli.Configuration.Field; + +namespace SW.Bitween.Cli.Configuration; + +/// Every section a configuration file can have, in the order apply works through them. +public static class Kinds +{ + /// Not a resource like the others: packages to publish, which are never read back or removed. + public const string Adapters = "adapters"; + + public static readonly IReadOnlyList All = + [ + new SubscriptionCategories(), + new WorkGroups(), + new InformationTypes(), + new Partners(), + new GlobalValues(), + new RetryPolicies(), + new DataSources(), + new DataSourceStatements(), + new Subscriptions(), + new ApiGateways(), + new BusGateways(), + new Notifiers(), + ]; + + public static readonly IReadOnlyList Sections = [.. All.Select(k => k.Section).Take(8), Adapters, .. All.Select(k => k.Section).Skip(8)]; + + public static Kind Of(string section) => All.First(k => k.Section == section); +} + +public class SubscriptionCategories : Kind +{ + public override string Section => "subscriptionCategories"; + public override string Noun => "subscription category"; + public override string Resource => "subscriptioncategories"; + public override string KeyField => "code"; + public override IReadOnlyList Fields { get; } = [Same("code"), Same("description")]; + + protected override Task> ListAsync(Context ctx) => Listed(ctx, "api/subscriptioncategories?limit=100000"); + protected override Task<(string, JsonObject)> DetailOf(Context ctx, JsonObject row) => Task.FromResult((row["id"]!.ToString(), row)); + public override Task DeleteAsync(Context ctx, Existing have) => ctx.Api.PostAsync($"api/{Resource}/{have.Id}/delete", new { }); +} + +public class WorkGroups : Kind +{ + public override string Section => "workGroups"; + public override string Noun => "work group"; + public override string Resource => "workgroups"; + public override IReadOnlyList Fields { get; } = [Same("name"), Same("busMessageName"), Same("options")]; + + protected override Task> ListAsync(Context ctx) => Listed(ctx, "api/workgroups?limit=1000000"); + protected override Task<(string, JsonObject)> DetailOf(Context ctx, JsonObject row) => Task.FromResult((row["id"]!.ToString(), row)); + public override Task DeleteAsync(Context ctx, Existing have) => ctx.Api.PostAsync($"api/{Resource}/{have.Id}/delete", new { }); +} + +public class InformationTypes : Kind +{ + /// Built in: what aggregations produce. Not configuration anyone writes. + const string AggregationDocumentId = "10001"; + + public override string Section => "informationTypes"; + public override string Noun => "information type"; + public override string Resource => "documents"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), Same("code"), new("format", "documentFormat"), Same("busEnabled"), Same("busMessageTypeName"), + Same("duplicateInterval"), Same("disregardsUnfilteredMessages"), + new("promotedProperties", "promotedProperties", FieldMode.KeyValues), Same("validationSchema"), + ]; + + protected override bool Include(string id, JsonObject api) => id != AggregationDocumentId; + + protected override JsonObject UpdateBody(Context ctx, Existing have, JsonObject want) + { + var body = base.UpdateBody(ctx, have, want); + body["id"] = Id(have.Id); + return body; + } +} + +/// +/// Partners, with their API keys under keys: { name: value }. Bitween keeps only a hash of a key and +/// shows its first five characters, so a key is the same if it starts with those; one that doesn't is +/// replaced, which takes two calls, since a key keeps its stored value for as long as its name stays. +/// +public class Partners : Kind +{ + const string SystemPartnerId = "1"; + + public override string Section => "partners"; + public override string Noun => "partner"; + public override string Resource => "partners"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), new("properties", "adapterProperties", FieldMode.Dictionary), + new("secretProperties", "secretProperties", FieldMode.Set), Same("loginIdentity"), + ]; + + protected override bool Include(string id, JsonObject api) => id != SystemPartnerId; + + public override JsonObject ToFile(Context ctx, JsonObject api) + { + var file = base.ToFile(ctx, api); + file["keys"] = Values.ToMap(api["apiCredentials"]); + return file; + } + + /// "abcde...(hidden)" says the key starts abcde; anything else says nothing. + static string Shown(JsonNode masked) + { + var s = masked?.ToString() ?? ""; + var end = s.IndexOf("...", StringComparison.Ordinal); + return end > 0 ? s[..end] : null; + } + + static bool SameKey(JsonNode want, JsonNode have) => Shown(have) is not { } start || (want?.ToString() ?? "").StartsWith(start, StringComparison.Ordinal); + + public override List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = base.Differences(ctx, want, have); + if (want["keys"] is JsonObject keys) + { + var current = have.File["keys"] as JsonObject ?? []; + if (keys.Count != current.Count || keys.Any(k => !current.ContainsKey(k.Key) || !SameKey(k.Value, current[k.Key]))) + differ.Add("keys"); + } + return differ; + } + + protected override JsonObject CreateBody(Context ctx, JsonObject want) + { + var body = ToApi(ctx, want); + body["adapterProperties"] ??= new JsonObject(); + body["secretProperties"] ??= new JsonArray(); + return body; + } + + public override async Task CreateAsync(Context ctx, JsonObject want) + { + var id = await base.CreateAsync(ctx, want); + if (want["keys"] is JsonObject { Count: > 0 }) + { + var api = (JsonObject)await ctx.Api.GetAsync($"api/partners/{id}"); + await UpdateAsync(ctx, ToExisting(ctx, id, api), want); + } + return id; + } + + public override async Task UpdateAsync(Context ctx, Existing have, JsonObject want) + { + var body = UpdateBody(ctx, have, want); + if (want["keys"] is JsonObject keys) + { + var current = have.File["keys"] as JsonObject ?? []; + var replaced = keys.Where(k => current.ContainsKey(k.Key) && !SameKey(k.Value, current[k.Key])).Select(k => k.Key).ToHashSet(); + if (replaced.Count > 0) + { + // First without them, so their stored hashes go; then with them, hashed anew. + body["apiCredentials"] = Values.ToKeyValues(new JsonObject(keys.Where(k => !replaced.Contains(k.Key)) + .Select(k => KeyValuePair.Create(k.Key, k.Value?.DeepClone())))); + await ctx.Api.PostAsync($"api/partners/{have.Id}", body); + } + body["apiCredentials"] = Values.ToKeyValues(keys); + } + await ctx.Api.PostAsync($"api/partners/{have.Id}", body); + } +} + +public class GlobalValues : Kind +{ + /// Where apply remembers what it manages; not configuration of anyone's. + public const string StatePrefix = "bitween-apply."; + + public override string Section => "globalValues"; + public override string Noun => "global values set"; + public override string Resource => "globaladaptervaluessets"; + public override string KeyField => "id"; + + public override IReadOnlyList Fields { get; } = + [Same("id"), Same("name"), new("values", "values", FieldMode.Dictionary), new("secretProperties", "secretProperties", FieldMode.Set)]; + + protected override Task<(string, JsonObject)> DetailOf(Context ctx, JsonObject row) => Task.FromResult((row["id"]!.ToString(), row)); + protected override bool Include(string id, JsonObject api) => !id.StartsWith(StatePrefix, StringComparison.Ordinal); + + protected override JsonObject CreateBody(Context ctx, JsonObject want) + { + var body = ToApi(ctx, want); + body["values"] ??= new JsonObject(); + body["secretProperties"] ??= new JsonArray(); + return body; + } + + public override Task DeleteAsync(Context ctx, Existing have) => ctx.Api.PostAsync($"api/{Resource}/{Uri.EscapeDataString(have.Id)}/delete", new { }); +} + +/// +/// Retry policies. Bitween keys each group's usage and alert overrides by the group's id, and drops +/// them for a group missing from an update, so groups keep their ids by name; the file never says them. +/// +public class RetryPolicies : Kind +{ + public override string Section => "retryPolicies"; + public override string Noun => "retry policy"; + public override string Resource => "retrypolicies"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), Same("groups"), new("alertHandler", "alertHandlerId"), + new("alertHandlerProperties", "alertHandlerProperties", FieldMode.Dictionary), + ]; + + public override JsonObject ToFile(Context ctx, JsonObject api) + { + var file = base.ToFile(ctx, api); + foreach (var group in (file["groups"] as JsonArray ?? []).OfType()) group.Remove("id"); + return file; + } + + /// + /// A budget that leaves maxAttemptsTotal out allows no retries at all: Bitween reads a missing total as + /// zero, as stored policies always have. Written in a file that is never what was meant, so it is + /// refused; 0 written out, or null for no shared ceiling, is taken as said. + /// + public override IEnumerable Unresolved(JsonObject want, Func known) + { + foreach (var problem in base.Unresolved(want, known)) yield return problem; + foreach (var group in (want["groups"] as JsonArray ?? []).OfType()) + if (group["budget"] is JsonObject budget && !string.Equals((string)group["action"], "Block", StringComparison.OrdinalIgnoreCase) && + !budget.ContainsKey("maxAttemptsTotal")) + yield return $"groups: {Context.Quote((string)group["name"] ?? "(unnamed)")} has no budget.maxAttemptsTotal, which Bitween reads as no retries at all; " + + "give the shared total, or null for no ceiling"; + } + + protected override JsonObject UpdateBody(Context ctx, Existing have, JsonObject want) + { + var body = base.UpdateBody(ctx, have, want); + var ids = (have.Api["groups"] as JsonArray ?? []).OfType() + .Where(g => g["name"] != null).GroupBy(g => g["name"]!.ToString()).ToDictionary(g => g.Key, g => g.First()["id"]?.DeepClone()); + foreach (var group in (body["groups"] as JsonArray ?? []).OfType()) + if (group["id"] == null && group["name"] is { } name && ids.TryGetValue(name.ToString(), out var id)) + group["id"] = id; + return body; + } +} + +public class DataSources : Kind +{ + public override string Section => "dataSources"; + public override string Noun => "data source"; + public override string Resource => "datasources"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), new("adapter", "adapterId"), Same("kind"), Same("placement"), + new("properties", "properties", FieldMode.Dictionary), new("secretProperties", "secretProperties", FieldMode.Set), + Same("inactive"), Same("softMemoryLimitMb"), Same("hardMemoryLimitMb"), Same("cpuPercentLimit"), Same("cpuLimitSamples"), + Same("deduplicationWindowDays"), + ]; + + public override List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = base.Differences(ctx, want, have); + // Bitween adds names that look secret on its own, so it can hold more than the file says. + if (differ.Remove("secretProperties")) + { + var held = (have.File["secretProperties"] as JsonArray ?? []).Select(x => x?.ToString()).ToHashSet(); + if ((want["secretProperties"] as JsonArray ?? []).Any(x => !held.Contains(x?.ToString()))) differ.Add("secretProperties"); + } + return differ; + } +} + +/// A data source's named SQL, keyed by data source and name: "warehouse/new orders". +public class DataSourceStatements : Kind +{ + public override string Section => "dataSourceStatements"; + public override string Noun => "SQL statement"; + public override string Resource => "datasourcestatements"; + + public override IReadOnlyList Fields { get; } = + [ + Reference("dataSource", "dataSourceId", "dataSources"), Same("name"), Same("sql"), Same("description"), + Reference("workGroup", "workGroupId", "workGroups"), Same("inactive"), Same("cursorColumn"), Same("keyColumn"), + ]; + + public override IReadOnlyList Fixed => ["dataSource"]; + + public override string KeyOf(JsonObject file) => $"{file["dataSource"]}/{file["name"]}"; + + public override string KeyOfApi(Context ctx, JsonObject api) => $"{ctx.KeyOf("dataSources", api["dataSourceId"])}/{api["name"]}"; + + protected override Task<(string, JsonObject)> DetailOf(Context ctx, JsonObject row) => Task.FromResult((row["id"]!.ToString(), row)); +} + +/// +/// Subscriptions. Each adapter is a block: handler: { adapter, version, properties }; no version +/// follows whatever is current. paused: true pauses one and false resumes it. What kind it is, its +/// information type, partner and what it aggregates are fixed once it exists. +/// +public class Subscriptions : Kind +{ + static readonly string[] Roles = ["receiver", "validator", "mapper", "handler"]; + + public override string Section => "subscriptions"; + public override string Noun => "subscription"; + public override string Resource => "subscriptions"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), Same("type"), + Reference("informationType", "documentId", "informationTypes"), + Reference("partner", "partnerId", "partners"), + Reference("aggregationFor", "aggregationForId", "subscriptions"), + Same("inactive"), + Reference("dataSource", "dataSourceId", "dataSources"), + Reference("category", "categoryId", "subscriptionCategories"), + Reference("workGroup", "workGroupId", "workGroups"), + Reference("retryPolicy", "retryPolicyId", "retryPolicies"), + Same("customRetryPolicy"), Same("matchExpression"), + new("documentFilter", "documentFilter", FieldMode.KeyValues), + Same("schedules"), + Reference("responseSubscription", "responseSubscriptionId", "subscriptions"), + Same("responseMessageTypeName"), Same("runOnBadResponses"), Same("autoPauseAfterFailures"), Same("aggregationTarget"), + Same("expectTraffic"), + ]; + + public override IReadOnlyList Fixed => ["type", "informationType", "partner", "aggregationFor"]; + + public override JsonObject ToFile(Context ctx, JsonObject api) + { + var file = base.ToFile(ctx, api); + foreach (var role in Roles) + { + if (string.IsNullOrEmpty((string)api[role + "Id"])) continue; + var block = new JsonObject { ["adapter"] = (string)api[role + "Id"] }; + if (!string.IsNullOrWhiteSpace((string)api[role + "Version"])) block["version"] = (string)api[role + "Version"]; + block["properties"] = Values.ToMap(api[role + "Properties"]); + file[role] = block; + } + file["paused"] = api["pausedOn"] != null; + // Bitween answers every member, set or not; the file names only what is set. + if (file["expectTraffic"] is JsonObject expect) + foreach (var key in expect.Where(p => Values.Empty(p.Value)).Select(p => p.Key).ToList()) expect.Remove(key); + // Every aggregation is of the one built-in type; the file doesn't say it. + if ((string)api["type"] == "Aggregation") file.Remove("informationType"); + return file; + } + + public override IEnumerable Unresolved(JsonObject want, Func known) + { + foreach (var problem in base.Unresolved(want, known)) yield return problem; + // Bitween keeps none for an aggregation, so one in the file was taken once and then read back as a + // change to a field that can't change, on every apply after. + if ((string)want["type"] == "Aggregation" && want.ContainsKey("informationType")) + yield return "informationType: an aggregation has none of its own (it rolls up the exchanges of what it aggregates); leave it out"; + } + + public override List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = base.Differences(ctx, want, have); + foreach (var role in Roles.Where(want.ContainsKey)) + { + var w = want[role] as JsonObject; + var h = have.File[role] as JsonObject; + var same = w == null + ? h == null + : h != null && (string)w["adapter"] == (string)h["adapter"] && Values.Same(w["version"], h["version"]) && + (!w.ContainsKey("properties") || Values.Same(w["properties"], h["properties"], exactKeys: true)); + if (!same) differ.Add(role); + } + if (want["paused"] is JsonValue paused && (bool)paused != ((bool?)have.File["paused"] ?? false)) differ.Add("paused"); + // A member the file leaves out is unset (days: every day, no hours, UTC), not "keep what Bitween has", + // so one set in Bitween alone is a difference too. + if (want.ContainsKey("expectTraffic")) + { + differ.Remove("expectTraffic"); + if (!Values.Same(want["expectTraffic"], have.File["expectTraffic"], exactKeys: true)) differ.Add("expectTraffic"); + } + return differ; + } + + public override JsonObject ToApi(Context ctx, JsonObject want) + { + var api = base.ToApi(ctx, want); + foreach (var role in Roles.Where(want.ContainsKey)) + { + var block = want[role] as JsonObject; + api[role + "Id"] = (string)block?["adapter"]; + api[role + "Version"] = (string)block?["version"]; + if (block == null || block.ContainsKey("properties")) + api[role + "Properties"] = Values.ToKeyValues(block?["properties"]); + } + // One or the other: a named policy, or one of its own. + if (want["retryPolicy"] != null) api["customRetryPolicy"] = null; + if (want["customRetryPolicy"] != null) api["retryPolicyId"] = null; + return api; + } + + protected override JsonObject CreateBody(Context ctx, JsonObject want) + { + var body = ToApi(ctx, want); + // Bitween makes a subscription switched off unless told otherwise; a file means what it says. + body["inactive"] = (bool?)want["inactive"] ?? false; + foreach (var role in Roles) body[role + "Properties"] ??= new JsonArray(); + body["documentFilter"] ??= new JsonArray(); + return body; + } + + public override async Task CreateAsync(Context ctx, JsonObject want) + { + var id = await base.CreateAsync(ctx, want); + if ((bool?)want["paused"] == true) await ctx.Api.PostAsync($"api/subscriptions/{id}/pause", new { }); + return id; + } + + public override async Task UpdateAsync(Context ctx, Existing have, JsonObject want) + { + await base.UpdateAsync(ctx, have, want); + // A toggle: only when it's the other way round. + if (want["paused"] is JsonValue paused && (bool)paused != ((bool?)have.File["paused"] ?? false)) + await ctx.Api.PostAsync($"api/subscriptions/{have.Id}/pause", new { }); + } +} + +/// API gateways, keyed by their address, with the partners attached: partners: [{ partner, subscription }]. +public class ApiGateways : Kind +{ + public override string Section => "apiGateways"; + public override string Noun => "API gateway"; + public override string Resource => "apigateways"; + public override string KeyField => "urlName"; + + public override IReadOnlyList Fields { get; } = + [Same("name"), Same("urlName"), Same("inactive"), Same("authentication"), Same("failureAnswer")]; + + public override IEnumerable DependsOn => ["partners", "subscriptions"]; + + protected override Task> ListAsync(Context ctx) => Searchy(ctx, Resource); + + public override JsonObject ToFile(Context ctx, JsonObject api) + { + var file = base.ToFile(ctx, api); + file["partners"] = new JsonArray((api["partners"] as JsonArray ?? []).OfType() + .Select(p => new JsonObject { ["partner"] = ctx.KeyOf("partners", p["partnerId"]), ["subscription"] = ctx.KeyOf("subscriptions", p["subscriptionId"]) }) + .OrderBy(p => (string)p["partner"], StringComparer.Ordinal).Select(p => (JsonNode)p).ToArray()); + return file; + } + + static Dictionary Attachments(JsonNode list) => + (list as JsonArray ?? []).OfType().Where(p => p["partner"] != null) + .GroupBy(p => p["partner"]!.ToString()).ToDictionary(g => g.Key, g => (string)g.First()["subscription"]); + + public override List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = base.Differences(ctx, want, have); + if (want.ContainsKey("partners")) + { + var w = Attachments(want["partners"]); + var h = Attachments(have.File["partners"]); + if (w.Count != h.Count || w.Any(p => !h.TryGetValue(p.Key, out var s) || s != p.Value)) differ.Add("partners"); + } + return differ; + } + + public override IEnumerable Unresolved(JsonObject want, Func known) + { + foreach (var problem in base.Unresolved(want, known)) yield return problem; + foreach (var (partner, subscription) in Attachments(want["partners"])) + { + if (!known("partners", partner)) yield return $"partners: there's no partner {Context.Quote(partner)}"; + if (subscription != null && !known("subscriptions", subscription)) yield return $"partners: there's no subscription {Context.Quote(subscription)}"; + } + } + + public override JsonObject ToApi(Context ctx, JsonObject want) + { + var api = base.ToApi(ctx, want); + api.Remove("partners"); + return api; + } + + public override async Task CreateAsync(Context ctx, JsonObject want) + { + var id = await base.CreateAsync(ctx, want); + await AttachAsync(ctx, id, [], want); + return id; + } + + public override async Task UpdateAsync(Context ctx, Existing have, JsonObject want) + { + await base.UpdateAsync(ctx, have, want); + await AttachAsync(ctx, have.Id, Attachments(have.File["partners"]), want); + } + + static async Task AttachAsync(Context ctx, string id, Dictionary current, JsonObject want) + { + if (!want.ContainsKey("partners")) return; + var wanted = Attachments(want["partners"]); + foreach (var (partner, subscription) in wanted) + { + var body = new { partnerId = int.Parse(ctx.IdOf("partners", partner)), subscriptionId = int.Parse(ctx.IdOf("subscriptions", subscription)) }; + if (!current.TryGetValue(partner, out var had)) await ctx.Api.PostAsync($"api/apigateways/{id}/addpartner", body); + else if (had != subscription) await ctx.Api.PostAsync($"api/apigateways/{id}/updatepartner", body); + } + foreach (var partner in current.Keys.Where(p => !wanted.ContainsKey(p))) + await ctx.Api.PostAsync($"api/apigateways/{id}/removepartner", new { partnerId = int.Parse(ctx.IdOf("partners", partner)) }); + } +} + +/// Bus gateways, with their routes: routes: [{ subscription, partner, matchExpression }]. +public class BusGateways : Kind +{ + public override string Section => "busGateways"; + public override string Noun => "bus gateway"; + public override string Resource => "busgateways"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), Reference("informationType", "documentId", "informationTypes"), Same("inactive"), + Reference("dataSource", "dataSourceId", "dataSources"), Same("endpoint"), + new("endpointProperties", "endpointProperties", FieldMode.Dictionary), + ]; + + public override IReadOnlyList Fixed => ["informationType"]; + public override IEnumerable DependsOn => [.. base.DependsOn, "partners", "subscriptions"]; + + public override JsonObject ToFile(Context ctx, JsonObject api) + { + var file = base.ToFile(ctx, api); + file["routes"] = new JsonArray((api["routes"] as JsonArray ?? []).OfType().Select(r => (JsonNode)Values.Tidy(new JsonObject + { + ["subscription"] = ctx.KeyOf("subscriptions", r["subscriptionId"]), + ["partner"] = ctx.KeyOf("partners", r["partnerId"]), + ["matchExpression"] = r["matchExpression"]?.DeepClone(), + })).ToArray()); + return file; + } + + static string RouteKey(JsonObject route) => $"{route["subscription"]}|{route["partner"]}"; + + static Dictionary Routes(JsonNode list) => + (list as JsonArray ?? []).OfType().GroupBy(RouteKey).ToDictionary(g => g.Key, g => g.First()); + + public override List Differences(Context ctx, JsonObject want, Existing have) + { + var differ = base.Differences(ctx, want, have); + if (want.ContainsKey("routes")) + { + var w = Routes(want["routes"]); + var h = Routes(have.File["routes"]); + if (w.Count != h.Count || w.Any(r => !h.TryGetValue(r.Key, out var had) || !Values.Same(r.Value["matchExpression"], had["matchExpression"]))) + differ.Add("routes"); + } + return differ; + } + + public override IEnumerable Unresolved(JsonObject want, Func known) + { + foreach (var problem in base.Unresolved(want, known)) yield return problem; + foreach (var route in Routes(want["routes"]).Values) + { + if ((string)route["subscription"] is { } s && !known("subscriptions", s)) yield return $"routes: there's no subscription {Context.Quote(s)}"; + if ((string)route["partner"] is { } p && !known("partners", p)) yield return $"routes: there's no partner {Context.Quote(p)}"; + } + } + + public override JsonObject ToApi(Context ctx, JsonObject want) + { + var api = base.ToApi(ctx, want); + api.Remove("routes"); + return api; + } + + protected override JsonObject CreateBody(Context ctx, JsonObject want) + { + var body = ToApi(ctx, want); + body["endpointProperties"] ??= new JsonObject(); + return body; + } + + public override async Task CreateAsync(Context ctx, JsonObject want) + { + var id = await base.CreateAsync(ctx, want); + await RouteAsync(ctx, id, [], [], want); + return id; + } + + public override async Task UpdateAsync(Context ctx, Existing have, JsonObject want) + { + await base.UpdateAsync(ctx, have, want); + var ids = (have.Api["routes"] as JsonArray ?? []).OfType().ToDictionary( + r => $"{ctx.KeyOf("subscriptions", r["subscriptionId"])}|{ctx.KeyOf("partners", r["partnerId"])}", r => (int)r["id"]!); + await RouteAsync(ctx, have.Id, Routes(have.File["routes"]), ids, want); + } + + static async Task RouteAsync(Context ctx, string id, Dictionary current, Dictionary routeIds, JsonObject want) + { + if (!want.ContainsKey("routes")) return; + var wanted = Routes(want["routes"]); + foreach (var (key, route) in wanted) + { + var partner = (string)route["partner"]; + var body = new JsonObject + { + ["subscriptionId"] = int.Parse(ctx.IdOf("subscriptions", (string)route["subscription"])), + ["partnerId"] = partner == null ? null : int.Parse(ctx.IdOf("partners", partner)), + ["matchExpression"] = route["matchExpression"]?.DeepClone(), + }; + if (!current.TryGetValue(key, out var had)) await ctx.Api.PostAsync($"api/busgateways/{id}/addroute", body); + else if (!Values.Same(route["matchExpression"], had["matchExpression"])) + { + body["routeId"] = routeIds[key]; + await ctx.Api.PostAsync($"api/busgateways/{id}/updateroute", body); + } + } + foreach (var key in current.Keys.Where(k => !wanted.ContainsKey(k))) + await ctx.Api.PostAsync($"api/busgateways/{id}/removeroute", new { routeId = routeIds[key] }); + } +} + +public class Notifiers : Kind +{ + public override string Section => "notifiers"; + public override string Noun => "notifier"; + public override string Resource => "notifiers"; + + public override IReadOnlyList Fields { get; } = + [ + Same("name"), new("handler", "handlerId"), new("properties", "handlerProperties", FieldMode.KeyValues), + Same("runOnSuccessfulResult"), Same("runOnBadResult"), Same("runOnFailedResult"), Same("inactive"), + new("subscriptions", "runOnSubscriptions", FieldMode.RefList, "subscriptions"), + ]; + + /// Bitween makes one from its name alone; the rest is an update. + public override async Task CreateAsync(Context ctx, JsonObject want) + { + var id = IdIn(await ctx.Api.PostAsync("api/notifiers", new { name = (string)want["name"] })); + var api = (JsonObject)await ctx.Api.GetAsync($"api/notifiers/{id}"); + await UpdateAsync(ctx, ToExisting(ctx, id, api), want); + return id; + } +} diff --git a/SW.Bitween.Cli/Configuration/Planner.cs b/SW.Bitween.Cli/Configuration/Planner.cs new file mode 100644 index 00000000..69b1cf2d --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Planner.cs @@ -0,0 +1,329 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using SW.Serverless.Installer; +using SW.Serverless.Tooling; + +namespace SW.Bitween.Cli.Configuration; + +public enum Op { Create, Update, Delete, Publish, Promote } + +/// One thing apply would do. +public record Change(Op Op, string Section, string Key, JsonObject Want = null, Existing Have = null, List Fields = null) +{ + public string Noun => Section == Kinds.Adapters ? "adapter" : Kinds.Of(Section).Noun; +} + +/// What apply would do, or what stops it. +public class Plan +{ + public List Changes { get; } = []; + public List Problems { get; } = []; + public List Warnings { get; } = []; + public int Unchanged { get; set; } + + /// What this configuration manages after the plan is carried out: for --prune next time. + public Dictionary> Managed { get; } = new(); +} + +/// +/// Compares a configuration file with a Bitween and brings the Bitween in line: creating what's +/// missing, updating what differs, and with --prune removing what the file no longer lists. Only +/// ever removes what an earlier apply of the same configuration put there; it remembers which, in +/// Bitween itself, so a job in a fresh container knows as much as one that ran before. +/// +public class Planner(Context ctx, ConfigFile config) +{ + string StateId => GlobalValues.StatePrefix + config.Name; + + /// Reads every section, in an order where whatever a field names has been read first. + public async Task ReadAsync() + { + var raw = new Dictionary>(); + foreach (var kind in Kinds.All) + { + raw[kind.Section] = await kind.ReadAsync(ctx); + // Statements' keys name their data source, read just before. + foreach (var (id, api) in raw[kind.Section]) ctx.Know(kind.Section, kind.KeyOfApi(ctx, api), id); + } + foreach (var kind in Kinds.All) + ctx.Current[kind.Section] = raw[kind.Section].Select(x => kind.ToExisting(ctx, x.Id, x.Api)).ToList(); + } + + public async Task PlanAsync(bool prune) + { + var plan = new Plan(); + var managedBefore = await ManagedAsync(); + recorded = Serialized(managedBefore.ToDictionary(m => m.Key, m => new SortedSet(m.Value, StringComparer.Ordinal))); + + await PlanAdaptersAsync(plan); + + foreach (var kind in Kinds.All) + { + var wanted = config.Sections.GetValueOrDefault(kind.Section) ?? []; + var keys = new HashSet(StringComparer.Ordinal); + var managed = plan.Managed[kind.Section] = new SortedSet(managedBefore.GetValueOrDefault(kind.Section) ?? [], StringComparer.Ordinal); + + foreach (var want in Ordered(kind, wanted)) + { + var key = kind.KeyOf(want); + if (string.IsNullOrWhiteSpace(key) || key.StartsWith('/') || key.EndsWith('/')) + { + plan.Problems.Add($"{kind.Section}: an item has no {kind.KeyField}."); + continue; + } + var where = $"{kind.Noun} {Context.Quote(key)}"; + if (!keys.Add(key)) { plan.Problems.Add($"{where} is in the file twice."); continue; } + if (ctx.Ambiguous(kind.Section, key)) { plan.Problems.Add($"Bitween has more than one {kind.Noun} named {Context.Quote(key)}; rename all but one."); continue; } + foreach (var problem in kind.Unresolved(want, Known)) plan.Problems.Add($"{where}: {problem}."); + managed.Add(key); + + var have = ctx.Current[kind.Section].FirstOrDefault(e => e.Key == key); + if (have == null) + { + plan.Changes.Add(new Change(Op.Create, kind.Section, key, want)); + continue; + } + + var fixedChanged = kind.Fixed.Where(f => want.ContainsKey(f) && !Values.Same(want[f], have.File[f])).ToList(); + foreach (var f in fixedChanged) + plan.Problems.Add($"{where}: {f} can't change once it exists ({Values.Show(have.File[f], ctx.Secrets)} to {Values.Show(want[f], ctx.Secrets)}). Remove it and add it again, under a new name or after removing the old one."); + var differ = kind.Differences(ctx, want, have).Except(fixedChanged).ToList(); + if (differ.Count > 0) plan.Changes.Add(new Change(Op.Update, kind.Section, key, want, have, differ)); + else plan.Unchanged++; + } + + // What an earlier apply put there and the file no longer lists. + foreach (var key in managed.Where(k => !keys.Contains(k)).ToList()) + { + var have = ctx.Current[kind.Section].FirstOrDefault(e => e.Key == key); + if (have == null) managed.Remove(key); + else if (prune) plan.Changes.Add(new Change(Op.Delete, kind.Section, key, Have: have)); + } + } + + // Removals last and in reverse, so nothing is removed while something still names it. + var removals = plan.Changes.Where(c => c.Op == Op.Delete) + .OrderByDescending(c => Kinds.All.ToList().FindIndex(k => k.Section == c.Section)).ToList(); + plan.Changes.RemoveAll(c => c.Op == Op.Delete); + plan.Changes.AddRange(removals); + if (!prune) + { + var left = Kinds.All.Sum(k => plan.Managed[k.Section].Count(key => !(config.Sections.GetValueOrDefault(k.Section) ?? []).Any(w => k.KeyOf(w) == key))); + if (left > 0) plan.Warnings.Add($"{left} item(s) an earlier apply made are no longer in the file; --prune removes them."); + } + return plan; + } + + bool Known(string section, string key) => + ctx.Has(section, key) || (config.Sections.GetValueOrDefault(section) ?? []).Any(w => Kinds.Of(section).KeyOf(w) == key); + + /// Its own kind first when one names another: a response subscription before the one that answers to it. + static IEnumerable Ordered(Kind kind, List wanted) + { + var pending = wanted.ToList(); + var placed = new HashSet(StringComparer.Ordinal); + var inFile = wanted.Select(kind.KeyOf).Where(k => k != null).ToHashSet(StringComparer.Ordinal); + while (pending.Count > 0) + { + var ready = pending.Where(w => kind.NamesOwnKind(w).All(k => !inFile.Contains(k) || placed.Contains(k) || k == kind.KeyOf(w))).ToList(); + if (ready.Count == 0) ready = [pending[0]]; // a loop: Bitween will say what's wrong with it + foreach (var w in ready) + { + pending.Remove(w); + if (kind.KeyOf(w) is { } k) placed.Add(k); + yield return w; + } + } + } + + async Task PlanAdaptersAsync(Plan plan) + { + foreach (var want in config.Sections.GetValueOrDefault(Kinds.Adapters) ?? []) + { + var package = (string)want["package"]; + if (string.IsNullOrWhiteSpace(package) || !File.Exists(package)) + { + plan.Problems.Add($"adapters: there's no package at {package ?? "(no package given)"}."); + continue; + } + var manifest = AdapterRepository.ReadManifest(package); + if (manifest?.Id == null || manifest.Version == null) + { + plan.Problems.Add($"adapters: {package} has no adapter.json with an id and a version; build it with bitween adapter build."); + continue; + } + + JsonNode listing = null; + try { listing = await ctx.Api.GetAsync($"api/adapters/versions?adapterId={Uri.EscapeDataString(manifest.Id)}"); } + catch (BitweenApiException ex) when (ex.Status == System.Net.HttpStatusCode.NotFound) { } + + var versions = (listing?["versions"] as JsonArray ?? []).OfType().ToList(); + var published = versions.FirstOrDefault(v => (string)v["version"] == manifest.Version); + var key = $"{manifest.Id} {manifest.Version}"; + var item = new JsonObject { ["package"] = package, ["id"] = manifest.Id, ["version"] = manifest.Version, ["current"] = (bool?)want["current"] ?? false }; + if (published == null) plan.Changes.Add(new Change(Op.Publish, Kinds.Adapters, key, item)); + else + { + // Published already, and versions don't change. Whether this package is the same one can't be + // told from the listing: Bitween stamps the manifest and zips the package again when it takes + // it, so the hash it keeps is never the uploaded file's, and comparing them warned every time. + if ((bool?)want["current"] == true && (string)listing?["current"] != manifest.Version) + plan.Changes.Add(new Change(Op.Promote, Kinds.Adapters, key, item)); + else plan.Unchanged++; + } + } + } + + // ---------------------------------------------------------------------- carrying it out + + public async Task ApplyAsync(Plan plan, TextWriter output) + { + try + { + foreach (var change in plan.Changes) + { + output.Write(" " + Describe(change) + " ... "); + try + { + await CarryOutAsync(change); + } + catch (Exception ex) when (ex is BitweenApiException or ConfigException) + { + output.WriteLine("failed"); + throw new ConfigException($"{Sentence(change)} failed: {ex.Message}"); + } + output.WriteLine("done"); + } + } + finally + { + await SaveManagedAsync(plan, output); + } + } + + async Task CarryOutAsync(Change change) + { + switch (change.Op) + { + case Op.Publish: + var query = (bool)change.Want["current"]! ? "?current=true" : ""; + await ctx.Api.PostFileAsync("api/adapters/packages" + query, (string)change.Want["package"], "application/zip"); + break; + case Op.Promote: + await ctx.Api.PostAsync("api/adapters/promote", new { adapterId = (string)change.Want["id"], version = (string)change.Want["version"] }); + break; + case Op.Create: + var kind = Kinds.Of(change.Section); + ctx.Know(change.Section, change.Key, await kind.CreateAsync(ctx, change.Want)); + break; + case Op.Update: + await Kinds.Of(change.Section).UpdateAsync(ctx, change.Have, change.Want); + break; + case Op.Delete: + await Kinds.Of(change.Section).DeleteAsync(ctx, change.Have); + ctx.Forget(change.Section, change.Key); + removed.Add(change); + break; + } + } + + readonly List removed = []; + + /// What was recorded before this run, to write it again only when it changed. + string recorded; + + static string Serialized(Dictionary> managed) => + JsonSerializer.Serialize(managed.Where(m => m.Value.Count > 0).OrderBy(m => m.Key, StringComparer.Ordinal) + .ToDictionary(m => m.Key, m => m.Value.ToList())); + + // ---------------------------------------------------------------------- what it manages + + async Task>> ManagedAsync() + { + JsonNode state; + try { state = await ctx.Api.GetAsync($"api/globaladaptervaluessets/{Uri.EscapeDataString(StateId)}"); } + catch (BitweenApiException ex) when (ex.Status is System.Net.HttpStatusCode.NotFound or System.Net.HttpStatusCode.Forbidden) { return new(); } + + var managed = new Dictionary>(); + foreach (var (section, value) in state?["values"] as JsonObject ?? []) + { + try { managed[section] = JsonSerializer.Deserialize>(value?.ToString() ?? "[]") ?? []; } + catch (JsonException) { } + } + return managed; + } + + async Task SaveManagedAsync(Plan plan, TextWriter output) + { + foreach (var change in removed) plan.Managed.GetValueOrDefault(change.Section)?.Remove(change.Key); + // Unchanged on most runs of a job that applies every half hour; no need to say so in the audit trail. + if (Serialized(plan.Managed) == recorded) return; + var values = new JsonObject(plan.Managed.Where(m => m.Value.Count > 0) + .Select(m => KeyValuePair.Create(m.Key, (JsonNode)JsonSerializer.Serialize(m.Value)))); + var body = new JsonObject + { + ["id"] = StateId, + ["name"] = $"Managed by bitween apply: {config.Name}", + ["values"] = values, + ["secretProperties"] = new JsonArray(), + }; + try + { + try + { + await ctx.Api.GetAsync($"api/globaladaptervaluessets/{Uri.EscapeDataString(StateId)}"); + await ctx.Api.PostAsync($"api/globaladaptervaluessets/{Uri.EscapeDataString(StateId)}", body); + } + catch (BitweenApiException ex) when (ex.Status == System.Net.HttpStatusCode.NotFound) + { + await ctx.Api.PostAsync("api/globaladaptervaluessets", body); + } + } + catch (BitweenApiException ex) + { + output.WriteLine($"Warning: couldn't record what this configuration manages ({ex.Message}); --prune won't know about it."); + } + } + + // ---------------------------------------------------------------------- saying it + + static string Sign(Op op) => op switch { Op.Create or Op.Publish => "+", Op.Delete => "-", _ => "~" }; + + public static string Describe(Change change) => change.Op switch + { + Op.Publish => $"+ publish adapter {change.Key}{((bool?)change.Want["current"] == true ? ", current" : "")}", + Op.Promote => $"~ make adapter {change.Key} current", + _ => $"{Sign(change.Op)} {change.Noun} {Context.Quote(change.Key)}", + }; + + static string Sentence(Change change) => change.Op switch + { + Op.Create => $"Creating {change.Noun} {Context.Quote(change.Key)}", + Op.Update => $"Updating {change.Noun} {Context.Quote(change.Key)}", + Op.Delete => $"Removing {change.Noun} {Context.Quote(change.Key)}", + Op.Publish => $"Publishing adapter {change.Key}", + _ => $"Making adapter {change.Key} current", + }; + + public void Print(Plan plan, TextWriter output) + { + foreach (var warning in plan.Warnings) output.WriteLine("Warning: " + warning); + if (plan.Problems.Count > 0) + { + output.WriteLine("The configuration can't be applied:"); + foreach (var problem in plan.Problems) output.WriteLine(" ! " + problem); + return; + } + foreach (var change in plan.Changes) + { + output.WriteLine(" " + Describe(change)); + foreach (var field in change.Fields ?? []) + output.WriteLine($" {field}: {Values.Show(change.Have.File[field], ctx.Secrets)} -> {Values.Show(change.Want[field], ctx.Secrets)}"); + } + int Count(params Op[] ops) => plan.Changes.Count(c => ops.Contains(c.Op)); + output.WriteLine(plan.Changes.Count == 0 + ? $"No changes. {plan.Unchanged} item(s) already as the file says." + : $"{Count(Op.Create)} to create, {Count(Op.Update)} to update, {Count(Op.Delete)} to remove, " + + $"{Count(Op.Publish, Op.Promote)} adapter step(s); {plan.Unchanged} unchanged."); + } +} diff --git a/SW.Bitween.Cli/Configuration/Values.cs b/SW.Bitween.Cli/Configuration/Values.cs new file mode 100644 index 00000000..36c03652 --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Values.cs @@ -0,0 +1,106 @@ +using System.Globalization; +using System.Text.Json.Nodes; + +namespace SW.Bitween.Cli.Configuration; + +/// How the file's values and the API's are compared and converted. +public static class Values +{ + /// What Bitween answers in place of a secret it holds. Sent back, it means "keep what you have". + public const string Masked = "__private__"; + + /// + /// Whether what the file asks for is what Bitween has. Only what the file says is compared: a key + /// it leaves out of a mapping is left as it is (with , at the top level of + /// a mapping that replaces everything, it must say every key). A secret Bitween won't show is taken + /// to match: there's no way to tell, and it is sent again whenever anything else changes. + /// + public static bool Same(JsonNode want, JsonNode have, bool exactKeys = false) + { + if (IsMasked(have)) return true; + if (Empty(want) && Empty(have)) return true; + + switch (want) + { + case JsonObject w when have is JsonObject h: + if (exactKeys && h.Any(p => !w.ContainsKey(p.Key) && !Empty(p.Value))) return false; + return w.All(p => Same(p.Value, h[p.Key])); + case JsonArray w when have is JsonArray h: + return w.Count == h.Count && w.Zip(h).All(x => Same(x.First, x.Second)); + case JsonValue w when have is JsonValue h: + return Text(w) == Text(h) || (Number(w) is { } a && Number(h) is { } b && a == b); + default: + return false; + } + } + + public static bool IsMasked(JsonNode node) => node is JsonValue v && v.TryGetValue(out var s) && s == Masked; + + public static bool Empty(JsonNode node) => node switch + { + null => true, + JsonArray a => a.Count == 0, + JsonObject o => o.Count == 0, + JsonValue v when v.TryGetValue(out var s) => s.Length == 0, + _ => false, + }; + + static string Text(JsonValue v) => v.TryGetValue(out var s) ? s + : v.TryGetValue(out var b) ? (b ? "true" : "false") + : v.ToJsonString(); + + static decimal? Number(JsonValue v) => + decimal.TryParse(v.TryGetValue(out var s) ? s : v.ToJsonString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var d) ? d : null; + + /// [{key, value}] as the file writes it: a mapping. + public static JsonObject ToMap(JsonNode keyValues) + { + var map = new JsonObject(); + if (keyValues is JsonArray list) + foreach (var item in list.OfType()) + if ((string)item["key"] is { } key) map[key] = item["value"]?.DeepClone(); + return map; + } + + /// A mapping as the API takes it: [{key, value}], every value a string. + public static JsonArray ToKeyValues(JsonNode map) => + new((map as JsonObject ?? []).Select(p => (JsonNode)new JsonObject + { + ["key"] = p.Key, + ["value"] = AsString(p.Value), + }).ToArray()); + + /// A mapping whose values are all strings, as the API's dictionaries are. + public static JsonObject ToStrings(JsonNode map) => + new((map as JsonObject ?? []).Select(p => KeyValuePair.Create(p.Key, (JsonNode)AsString(p.Value)))); + + static JsonValue AsString(JsonNode value) => value switch + { + null => null, + JsonValue v when v.TryGetValue(out var s) => JsonValue.Create(s), + JsonValue v => JsonValue.Create(Text(v)), + _ => JsonValue.Create(value.ToJsonString()), + }; + + /// Drops what says nothing (null, false, empty), so an exported file reads as what was set. + public static JsonObject Tidy(JsonObject obj) + { + foreach (var key in obj.Select(p => p.Key).ToList()) + if (Empty(obj[key]) || obj[key] is JsonValue v && v.TryGetValue(out var b) && !b) + obj.Remove(key); + return obj; + } + + /// A value as shown in a plan: secrets as (secret), long text shortened. + public static string Show(JsonNode node, ISet secrets) + { + if (node is JsonValue v && v.TryGetValue(out var s)) + { + if (secrets.Contains(s) || s == Masked) return "(secret)"; + return s.Length > 60 || s.Contains('\n') ? $"({s.Length} characters)" : s; + } + var text = node?.ToJsonString() ?? "null"; + foreach (var secret in secrets) text = text.Replace(secret, "(secret)"); + return text.Length > 80 ? text[..77] + "..." : text; + } +} diff --git a/SW.Bitween.Cli/Configuration/Yaml.cs b/SW.Bitween.Cli/Configuration/Yaml.cs new file mode 100644 index 00000000..b18595d4 --- /dev/null +++ b/SW.Bitween.Cli/Configuration/Yaml.cs @@ -0,0 +1,142 @@ +using System.Globalization; +using System.Text; +using System.Text.Json.Nodes; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; + +namespace SW.Bitween.Cli.Configuration; + +/// +/// YAML in and out, by way of JSON nodes: the API speaks JSON, so everything past the file is JSON +/// and only reading and writing the file knows it was YAML. +/// +public static class Yaml +{ + /// Every document in the text, as JSON. Unquoted scalars become numbers, booleans and nulls. + public static List Read(string text) + { + var stream = new YamlStream(); + stream.Load(new StringReader(text)); + return stream.Documents.Select(d => ToJson(d.RootNode)).ToList(); + } + + static JsonNode ToJson(YamlNode node) => node switch + { + YamlMappingNode map => new JsonObject(map.Children.Select(c => + KeyValuePair.Create(((YamlScalarNode)c.Key).Value ?? "", ToJson(c.Value)))), + YamlSequenceNode list => new JsonArray(list.Children.Select(ToJson).ToArray()), + YamlScalarNode scalar => Scalar(scalar), + _ => null, + }; + + static JsonNode Scalar(YamlScalarNode scalar) + { + var value = scalar.Value; + if (scalar.Style is not ScalarStyle.Plain) return JsonValue.Create(value); + switch (value) + { + case null or "" or "~" or "null" or "Null" or "NULL": return null; + case "true" or "True" or "TRUE": return JsonValue.Create(true); + case "false" or "False" or "FALSE": return JsonValue.Create(false); + } + if (long.TryParse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var whole)) + return JsonValue.Create(whole); + if (value.Any(c => c is '.' or 'e' or 'E') && + double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var fraction) && double.IsFinite(fraction)) + return JsonValue.Create(fraction); + return JsonValue.Create(value); + } + + /// JSON as YAML: block style, strings quoted only where they'd read as something else. + public static string Write(JsonNode node) + { + var text = new StringBuilder(); + WriteNode(text, node, 0, inList: false); + return text.ToString(); + } + + static void WriteNode(StringBuilder text, JsonNode node, int indent, bool inList) + { + switch (node) + { + case JsonObject obj when obj.Count > 0: + var first = true; + foreach (var (key, value) in obj) + { + if (!(first && inList)) text.Append(' ', indent); + first = false; + text.Append(Key(key)).Append(':'); + WriteValue(text, value, indent); + } + break; + case JsonArray list when list.Count > 0: + foreach (var item in list) + { + text.Append(' ', indent).Append('-'); + if (item is JsonArray { Count: > 0 }) + { + text.Append('\n'); + WriteNode(text, item, indent + 2, inList: false); + } + else if (item is JsonObject { Count: > 0 }) + { + text.Append(' '); + WriteNode(text, item, indent + 2, inList: true); + } + else text.Append(' ').Append(Inline(item)).Append('\n'); + } + break; + default: + text.Append(Inline(node)).Append('\n'); + break; + } + } + + static void WriteValue(StringBuilder text, JsonNode value, int indent) + { + switch (value) + { + case JsonObject { Count: > 0 }: + text.Append('\n'); + WriteNode(text, value, indent + 2, inList: false); + break; + case JsonArray { Count: > 0 }: + text.Append('\n'); + WriteNode(text, value, indent, inList: false); + break; + case JsonValue v when v.TryGetValue(out var s) && s.Contains('\n'): + // Schemas and SQL read best as they were written. + text.Append(" |").Append(s.EndsWith('\n') ? "" : "-").Append('\n'); + foreach (var line in s.TrimEnd('\n').Split('\n')) + text.Append(line.Length == 0 ? "" : new string(' ', indent + 2) + line.TrimEnd('\r')).Append('\n'); + break; + default: + text.Append(' ').Append(Inline(value)).Append('\n'); + break; + } + } + + static string Inline(JsonNode node) => node switch + { + null => "null", + JsonObject => "{}", + JsonArray => "[]", + JsonValue v when v.TryGetValue(out var s) => Quote(s), + JsonValue v when v.TryGetValue(out var b) => b ? "true" : "false", + _ => node.ToJsonString(), + }; + + static string Key(string key) => Plain(key) ? key : Quote(key, force: true); + + static string Quote(string s, bool force = false) + { + if (!force && Plain(s) && Scalar(new YamlScalarNode(s) { Style = ScalarStyle.Plain }) is JsonValue v && v.TryGetValue(out _)) return s; + return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\t", "\\t").Replace("\r", "\\r") + "\""; + } + + /// Safe to write without quotes: nothing YAML would read as structure. + static bool Plain(string s) => + s.Length > 0 && !char.IsWhiteSpace(s[0]) && !char.IsWhiteSpace(s[^1]) && + "-?:,[]{}#&*!|>'\"%@`".IndexOf(s[0]) < 0 && !s.Contains(": ") && !s.Contains(" #") && !s.EndsWith(':') && + s.All(c => !char.IsControl(c)); +} diff --git a/SW.Bitween.Cli/ConfigurationCommands.cs b/SW.Bitween.Cli/ConfigurationCommands.cs new file mode 100644 index 00000000..bb8359e7 --- /dev/null +++ b/SW.Bitween.Cli/ConfigurationCommands.cs @@ -0,0 +1,124 @@ +using CommandLine; +using SW.Bitween.Cli.Configuration; + +namespace SW.Bitween.Cli; + +// ---------------------------------------------------------------------------- configuration + +public class ConfigurationOptions +{ + [Option('f', "file", Required = true, HelpText = "The configuration: a .yaml file, or a folder of them.")] + public string File { get; set; } + + [Option("secrets-dir", HelpText = "A folder with a file per ${secret:NAME}, such as a mounted Kubernetes secret (or BITWEEN_SECRETS_DIR).")] + public string SecretsDir { get; set; } + + [Option("prune", HelpText = "Also remove what an earlier apply of this configuration made and the file no longer lists.")] + public bool Prune { get; set; } + + [Option("profile", HelpText = "Which signed-in Bitween; the current one, or BITWEEN_URL with BITWEEN_TOKEN, unless given.")] + public string Profile { get; set; } +} + +[Verb("plan", HelpText = "Show what apply would change, and change nothing.")] +public class PlanOptions : ConfigurationOptions +{ + [Option("detailed-exitcode", HelpText = "Exit 2 when there are changes to apply, 0 when there are none.")] + public bool DetailedExitCode { get; set; } +} + +[Verb("apply", HelpText = "Make a Bitween's configuration what the file says: create what's missing, update what differs.")] +public class ApplyOptions : ConfigurationOptions; + +[Verb("export", HelpText = "Write a Bitween's configuration as a file apply takes.")] +public class ExportOptions +{ + [Option('o', "out", HelpText = "The file to write; standard output unless given.")] + public string Out { get; set; } + + [Option("name", Default = "exported", HelpText = "The configuration's name, in metadata.name.")] + public string Name { get; set; } + + [Option("only", Separator = ',', HelpText = "Just these sections, separated by commas: partners,subscriptions.")] + public IEnumerable Only { get; set; } + + [Option("profile", HelpText = "Which signed-in Bitween; the current one, or BITWEEN_URL with BITWEEN_TOKEN, unless given.")] + public string Profile { get; set; } +} + +public static partial class Commands +{ + /// Exit code of plan --detailed-exitcode when there's something to apply. + public const int Changes = 2; + + public static Task Plan(PlanOptions opts, Profiles profiles) => PlanOrApply(opts, profiles, apply: false, opts.DetailedExitCode); + + public static Task Apply(ApplyOptions opts, Profiles profiles) => PlanOrApply(opts, profiles, apply: true, false); + + static async Task PlanOrApply(ConfigurationOptions opts, Profiles profiles, bool apply, bool detailed) + { + ConfigFile config; + try + { + config = ConfigFile.Load(opts.File, opts.SecretsDir ?? Environment.GetEnvironmentVariable("BITWEEN_SECRETS_DIR")); + } + catch (ConfigException ex) + { + Console.WriteLine(ex.Message); + return Failure; + } + + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + + var ctx = new Context(api, config.SecretValues); + var planner = new Planner(ctx, config); + Console.WriteLine($"Reading {api.Url}..."); + await planner.ReadAsync(); + var plan = await planner.PlanAsync(opts.Prune); + + Console.WriteLine($"{(apply ? "Applying" : "Plan for")} {config.Name} on {api.Url}:"); + planner.Print(plan, Console.Out); + if (plan.Problems.Count > 0) return Failure; + if (!apply) return detailed && plan.Changes.Count > 0 ? Changes : Success; + + try + { + if (plan.Changes.Count > 0) Console.WriteLine("Applying:"); + await planner.ApplyAsync(plan, Console.Out); + } + catch (ConfigException ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine("Stopped there; what was done before it stays done. Running apply again carries on."); + return Failure; + } + if (plan.Changes.Count > 0) Console.WriteLine($"Applied {plan.Changes.Count} change(s)."); + return Success; + } + + public static async Task Export(ExportOptions opts, Profiles profiles) + { + var only = opts.Only?.Select(s => s.Trim()).Where(s => s.Length > 0).ToList() ?? []; + var unknown = only.Where(s => Kinds.All.All(k => k.Section != s)).ToList(); + if (unknown.Count > 0) + { + Console.WriteLine($"There's no section {string.Join(", ", unknown)}. Known: {string.Join(", ", Kinds.All.Select(k => k.Section))}."); + return Failure; + } + + using var api = Api(profiles, opts.Profile); + if (api == null) return Failure; + + var (yaml, secrets, count) = await Exporter.ExportAsync(new Context(api), opts.Name, only); + if (opts.Out == null) Console.Write(yaml); + else + { + await File.WriteAllTextAsync(opts.Out, yaml); + Console.WriteLine($"Wrote {count} item(s) from {api.Url} to {opts.Out}."); + } + if (secrets.Count > 0) + Console.Error.WriteLine($"Bitween doesn't show secrets, so the file names them instead. Applying it needs: {string.Join(", ", secrets)}."); + return Success; + } +} diff --git a/SW.Bitween.Cli/Profiles.cs b/SW.Bitween.Cli/Profiles.cs new file mode 100644 index 00000000..67bf43b0 --- /dev/null +++ b/SW.Bitween.Cli/Profiles.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SW.Bitween.Cli; + +/// A Bitween signed in to: where it is, who, and the tokens that keep the session. +public class Profile +{ + public string Url { get; set; } + public string Email { get; set; } + public string AccessToken { get; set; } + + /// Renews the access token, as the browser's cookie does. Each renewal replaces it. + public string RefreshToken { get; set; } + + /// Accepts any certificate: for a Bitween on a development certificate only. + public bool Insecure { get; set; } +} + +public class ProfileFile +{ + public string Current { get; set; } + public Dictionary Profiles { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} + +/// +/// The signed-in Bitweens, kept in the user's own configuration folder, readable by them alone: +/// a refresh token is as good as a password until it is used or expires. +/// +public class Profiles(string path = null) +{ + static readonly JsonSerializerOptions Json = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + public string Path { get; } = path ?? Environment.GetEnvironmentVariable("BITWEEN_CLI_CONFIG") ?? DefaultPath(); + + static string DefaultPath() + { + var root = OperatingSystem.IsWindows() + ? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + : Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") is { Length: > 0 } xdg + ? xdg + : System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); + return System.IO.Path.Combine(root, "bitween", "cli.json"); + } + + public ProfileFile Load() + { + if (!File.Exists(Path)) return new ProfileFile(); + try + { + var file = JsonSerializer.Deserialize(File.ReadAllText(Path), Json) ?? new ProfileFile(); + file.Profiles = new Dictionary(file.Profiles ?? new(), StringComparer.OrdinalIgnoreCase); + return file; + } + catch (JsonException) + { + return new ProfileFile(); + } + } + + public void Save(ProfileFile file) + { + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)!); + var temp = Path + ".tmp"; + File.WriteAllText(temp, JsonSerializer.Serialize(file, Json)); + if (!OperatingSystem.IsWindows()) + File.SetUnixFileMode(temp, UnixFileMode.UserRead | UnixFileMode.UserWrite); + File.Move(temp, Path, overwrite: true); + } + + /// The profile asked for by name, or the current one; null when signed in nowhere. + public (string Name, Profile Profile) Find(string name = null) + { + var file = Load(); + var wanted = name ?? Environment.GetEnvironmentVariable("BITWEEN_PROFILE") ?? file.Current; + return wanted != null && file.Profiles.TryGetValue(wanted, out var profile) ? (wanted, profile) : (wanted, null); + } + + public void Put(string name, Profile profile, bool makeCurrent) + { + var file = Load(); + file.Profiles[name] = profile; + if (makeCurrent || file.Current == null) file.Current = name; + Save(file); + } + + public bool Remove(string name) + { + var file = Load(); + if (!file.Profiles.Remove(name)) return false; + if (string.Equals(file.Current, name, StringComparison.OrdinalIgnoreCase)) file.Current = file.Profiles.Keys.FirstOrDefault(); + Save(file); + return true; + } +} diff --git a/SW.Bitween.Cli/Program.cs b/SW.Bitween.Cli/Program.cs new file mode 100644 index 00000000..47553a5c --- /dev/null +++ b/SW.Bitween.Cli/Program.cs @@ -0,0 +1,146 @@ +using CommandLine; +using CommandLine.Text; +using SW.Bitween.Adapters.Tooling; + +namespace SW.Bitween.Cli; + +/// +/// bitween - Bitween from a terminal. +/// +/// bitween login https://bitween.example.com +/// bitween adapter init AcmeOrders --kind handler --lang python +/// bitween adapter build AcmeOrders +/// bitween adapter test AcmeOrders --settings settings.json +/// bitween adapter publish AcmeOrders/bin/serverless/acme.orders-0.1.0.zip +/// +public static class Program +{ + public static async Task Main(string[] args) => await RunAsync(args, new Profiles(), Console.In, ReadSecret); + + internal static async Task RunAsync(string[] args, Profiles profiles, TextReader input, Func readSecret) + { + BitweenAdapters.RegisterContract(); + try + { + switch (args.FirstOrDefault()) + { + case "adapter": + case "adapters": + return await Adapter(args[1..], profiles); + case "version": + case "--version": + Console.WriteLine($"bitween {Version}"); + return Commands.Success; + case null: + case "help": + case "--help": + case "-h": + Console.WriteLine(Usage); + return args.Length == 0 ? Commands.Failure : Commands.Success; + } + + var parsed = Parser().ParseArguments(args); + return await parsed.MapResult( + (LoginOptions o) => Commands.Login(o, profiles, input, readSecret), + (LogoutOptions o) => Commands.Logout(o, profiles), + (WhoAmIOptions o) => Commands.WhoAmI(o, profiles), + (PlanOptions o) => Commands.Plan(o, profiles), + (ApplyOptions o) => Commands.Apply(o, profiles), + (ExportOptions o) => Commands.Export(o, profiles), + _ => Task.FromResult(Help(parsed, "bitween"))); + } + catch (BitweenApiException ex) + { + Console.WriteLine(ex.Message); + return Commands.Failure; + } + catch (Exception ex) when (ex is PrimitiveTypes.SWException or HttpRequestException or IOException or UnauthorizedAccessException) + { + Console.WriteLine(ex.Message); + return Commands.Failure; + } + } + + static async Task Adapter(string[] args, Profiles profiles) + { + var parsed = Parser().ParseArguments(args); + return await parsed.MapResult( + (InitOptions o) => Task.FromResult(Commands.Init(o)), + (BuildOptions o) => Commands.Build(o), + (TestOptions o) => Commands.Test(o), + (RunOptions o) => Commands.Run(o), + (PublishOptions o) => Commands.Publish(o, profiles), + (PromoteOptions o) => Commands.Promote(o, profiles), + (VersionsOptions o) => Commands.Versions(o, profiles), + (WithdrawOptions o) => Commands.Withdraw(o, profiles), + _ => Task.FromResult(Help(parsed, "bitween adapter"))); + } + + static Parser Parser() => new(s => + { + s.HelpWriter = null; + s.CaseInsensitiveEnumValues = true; + }); + + static int Help(ParserResult parsed, string command) + { + var help = HelpText.AutoBuild(parsed, h => + { + h.Heading = $"bitween {Version}"; + h.Copyright = ""; + h.AdditionalNewLineAfterOption = false; + return HelpText.DefaultParsingErrorsHandler(parsed, h); + }, e => e, verbsIndex: true); + Console.WriteLine(help.ToString().Replace("bitween.dll", command)); + var asked = parsed.Errors.Any(e => e is HelpRequestedError or HelpVerbRequestedError or VersionRequestedError); + return asked ? Commands.Success : Commands.Failure; + } + + static string Version => + typeof(Program).Assembly.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false) + .OfType().FirstOrDefault()?.InformationalVersion.Split('+')[0] ?? "dev"; + + const string Usage = """ + bitween - Bitween from a terminal + + bitween login Sign in to a Bitween through the browser (--no-browser to paste + a code; --email and --password-stdin for a password) + bitween logout Sign out of the Bitween signed in to + bitween whoami Which Bitween, and as whom + + bitween plan -f What apply would change, changing nothing + bitween apply -f Make the Bitween's configuration what the file says (--prune + also removes what an earlier apply made and the file dropped) + bitween export [-o file] Write the Bitween's configuration as a file apply takes + + Without a profile, BITWEEN_URL and BITWEEN_TOKEN sign in as a service account. + + bitween adapter init Write a new adapter: --kind handler|mapper|validator|receiver + --lang dotnet|python|node|typescript + bitween adapter build [folder] Build it into a package + bitween adapter test [package] Check it against the Bitween contract (--settings file.json) + bitween adapter run [package] Call one command (--call Handle --input '{...}') + bitween adapter publish Publish through the signed-in Bitween (--current to make it run; + -p and storage flags to publish straight to storage instead) + bitween adapter promote + bitween adapter versions + bitween adapter withdraw + + bitween --help shows a command's options. + """; + + /// A password typed without echo. + static string ReadSecret() + { + if (Console.IsInputRedirected) return Console.In.ReadLine(); + var secret = new System.Text.StringBuilder(); + while (true) + { + var key = Console.ReadKey(intercept: true); + if (key.Key == ConsoleKey.Enter) return secret.ToString(); + if (key.Key == ConsoleKey.Backspace) { if (secret.Length > 0) secret.Length--; } + else if (!char.IsControl(key.KeyChar)) secret.Append(key.KeyChar); + } + } +} diff --git a/SW.Bitween.Cli/SW.Bitween.Cli.csproj b/SW.Bitween.Cli/SW.Bitween.Cli.csproj new file mode 100644 index 00000000..65d88411 --- /dev/null +++ b/SW.Bitween.Cli/SW.Bitween.Cli.csproj @@ -0,0 +1,30 @@ + + + + + Exe + net10.0 + bitween + SW.Bitween.Cli + latest + disable + enable + false + true + + + + + + + + + + + + + + + + diff --git a/SW.Bitween.HttpTests/AboutTests.cs b/SW.Bitween.HttpTests/AboutTests.cs new file mode 100644 index 00000000..c9682133 --- /dev/null +++ b/SW.Bitween.HttpTests/AboutTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// GET /api/settings/about: what this Bitween is and how it is set up, for whoever may see settings. +/// +[Collection("Http")] +public class AboutTests(HttpFixture fixture) +{ + [Fact] + public async Task An_administrator_sees_the_version_node_health_and_runtimes() + { + using var admin = await fixture.AdminAsync(); + var about = await Api.Json(await admin.GetAsync("/api/settings/about")); + + Assert.False(string.IsNullOrEmpty((string)about["version"])); + Assert.Equal(Environment.MachineName, (string)about["node"]!["name"]); + Assert.Equal("PgSql", (string)about["database"]); + + var health = about["health"]!.AsArray().ToDictionary(h => (string)h!["name"]!, h => (string)h!["status"]!); + Assert.Equal("Healthy", health["database"]); + Assert.Equal("Healthy", health["rabbitmq"]); + + var runtimes = about["adapters"]!["runtimes"]!.AsArray().Select(r => (string)r!["name"]).ToArray(); + Assert.Equal(["dotnet", "python", "node"], runtimes); + Assert.True((bool)about["adapters"]!["runtimes"]![1]!["available"]!, "python3 is needed by these tests, so it is here"); + // The effective value, which the test host raises from the default 600. + Assert.Equal(100000, (int)about["limits"]!["requestsPerMinute"]!); + + // Never a secret, only whether it is set. + Assert.DoesNotContain("password", about.ToJsonString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task A_member_who_cannot_see_settings_is_refused() + { + using var admin = await fixture.AdminAsync(); + var email = $"about-{Guid.NewGuid():N}@example.com"[..40]; + const string password = "About-Member-2026!"; + await Api.CreateAsync(admin, "/api/accounts", new { name = "About member", email, password, roleIds = new[] { 2 } }); + + using var member = fixture.Client(); + member.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", + await HttpFixture.SignInAsync(member, email, password)); + Assert.Equal(HttpStatusCode.Forbidden, (await member.GetAsync("/api/settings/about")).StatusCode); + } +} diff --git a/SW.Bitween.HttpTests/AccountAndRoleTests.cs b/SW.Bitween.HttpTests/AccountAndRoleTests.cs new file mode 100644 index 00000000..a19b3a15 --- /dev/null +++ b/SW.Bitween.HttpTests/AccountAndRoleTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Members and roles as an administrator manages them, over HTTP: a custom role grants exactly what +/// it lists, changing the role changes what its members can do, a reset password replaces the old +/// one and ends its sessions, and a role is only removed once nobody holds it. +/// +[Collection("Http")] +public class AccountAndRoleTests(HttpFixture fixture) +{ + static string Email() => $"member-{Guid.NewGuid():N}@example.com"[..40]; + + async Task SignedInAsync(string email, string password) + { + var client = fixture.Client(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", await HttpFixture.SignInAsync(client, email, password)); + return client; + } + + static async Task RefusedAsync(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + Assert.True(!response.IsSuccessStatusCode, $"expected a refusal, got {(int)response.StatusCode}: {body}"); + return body; + } + + [Fact] + public async Task A_custom_role_grants_what_it_lists_and_changing_it_changes_what_its_members_can_do() + { + using var admin = await fixture.AdminAsync(); + var roleName = Api.Unique("Type reader"); + var roleId = await Api.CreateAsync(admin, "/api/roles", new + { + name = roleName, description = "reads information types", permissions = new[] { "documents.view" } + }); + var role = await Api.Json(await admin.GetAsync($"/api/roles/{roleId}")); + Assert.Equal(["documents.view"], role["permissions"]!.AsArray().Select(p => (string)p!).ToArray()); + + var email = Email(); + const string password = "Member-Pass-2026!"; + var accountId = await Api.CreateAsync(admin, "/api/accounts", new + { + name = "Member One", email, password, roleIds = new[] { roleId } + }); + + // The same email can't be taken twice. + Assert.Contains("ACCOUNT_EXISTS", await RefusedAsync(await admin.PostAsJsonAsync("/api/accounts", new + { + name = "Member Again", email, password, roleIds = new[] { roleId } + }))); + + using var member = await SignedInAsync(email, password); + Assert.True((await member.GetAsync("/api/documents")).IsSuccessStatusCode); + var create = new { name = Api.Unique("Invoice"), documentFormat = "Json" }; + Assert.Equal(HttpStatusCode.Forbidden, (await member.PostAsJsonAsync("/api/documents", create)).StatusCode); + Assert.Equal(HttpStatusCode.Forbidden, (await member.GetAsync("/api/partners")).StatusCode); + + // Granted on the role, and the member's existing session can use it. + await Api.Json(await admin.PostAsJsonAsync($"/api/roles/{roleId}", new + { + name = roleName, permissions = new[] { "documents.view", "documents.create" } + })); + Assert.True((await member.PostAsJsonAsync("/api/documents", create)).IsSuccessStatusCode); + + // Taken away again, and so is the ability. + await Api.Json(await admin.PostAsJsonAsync($"/api/roles/{roleId}", new + { + name = roleName, permissions = new[] { "documents.view" } + })); + Assert.Equal(HttpStatusCode.Forbidden, (await member.PostAsJsonAsync("/api/documents", + new { name = Api.Unique("Invoice"), documentFormat = "Json" })).StatusCode); + + // A role somebody holds stays. + Assert.Contains("ROLE_IN_USE", await RefusedAsync(await admin.DeleteAsync($"/api/roles/{roleId}"))); + await Api.Json(await admin.PostAsJsonAsync($"/api/accounts/{accountId}/setRoles", new { roleIds = Array.Empty() })); + Assert.Equal(HttpStatusCode.Forbidden, (await member.GetAsync("/api/documents")).StatusCode); + await Api.Json(await admin.DeleteAsync($"/api/roles/{roleId}")); + Assert.False((await admin.GetAsync($"/api/roles/{roleId}")).IsSuccessStatusCode); + } + + [Fact] + public async Task A_built_in_role_cannot_be_deleted() + { + using var admin = await fixture.AdminAsync(); + var roles = (await Api.Json(await admin.GetAsync("/api/roles")))["result"]!.AsArray(); + var builtIn = roles.First(r => (bool)r!["isSystem"]!); + Assert.Contains("ROLE_IS_BUILT_IN", await RefusedAsync(await admin.DeleteAsync($"/api/roles/{(int)builtIn!["id"]!}"))); + } + + [Fact] + public async Task A_profile_update_renames_the_member_and_a_password_reset_replaces_the_old_one() + { + using var admin = await fixture.AdminAsync(); + var email = Email(); + var accountId = await Api.CreateAsync(admin, "/api/accounts", new + { + name = "Before Rename", email, password = "First-Pass-2026!", role = 2 + }); + + await Api.Json(await admin.PostAsJsonAsync($"/api/accounts/{accountId}", new { name = "After Rename" })); + var found = (await Api.Json(await admin.GetAsync("/api/accounts"))).ToJsonString(); + Assert.Contains("After Rename", found); + Assert.DoesNotContain("Before Rename", found); + + // A session from the old password, with its refresh cookie. + using var old = fixture.Client(); + var login = await old.PostAsJsonAsync("/api/accounts/login", new { Username = email, Password = "First-Pass-2026!" }); + Assert.True(login.IsSuccessStatusCode); + var refresh = login.Headers.GetValues("Set-Cookie").Select(c => c.Split(';')[0]) + .First(c => c.StartsWith("refresh_token=")); + + await Api.Json(await admin.PostAsJsonAsync($"/api/accounts/{accountId}/setPassword", new { password = "Second-Pass-2026!" })); + + using (var again = fixture.Client()) + Assert.False((await again.PostAsJsonAsync("/api/accounts/login", + new { Username = email, Password = "First-Pass-2026!" })).IsSuccessStatusCode); + using (var fresh = fixture.Client()) + Assert.True((await fresh.PostAsJsonAsync("/api/accounts/login", + new { Username = email, Password = "Second-Pass-2026!" })).IsSuccessStatusCode); + + // The reset ends what the old password opened. + using var tab = fixture.Client(); + using var request = new HttpRequestMessage(HttpMethod.Post, "/api/accounts/login") + { + Content = JsonContent.Create(new { }) + }; + request.Headers.Add("Cookie", refresh); + Assert.False((await tab.SendAsync(request)).IsSuccessStatusCode, "the old session's refresh token outlived a reset"); + + // An administrator resets other people's passwords, not their own. + var adminId = (int)(await Api.Json(await admin.GetAsync("/api/accounts/profile")))["id"]!; + Assert.Contains("USE_CHANGE_PASSWORD", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/accounts/{adminId}/setPassword", new { password = "Third-Pass-2026!" }))); + } +} diff --git a/SW.Bitween.HttpTests/AdapterEditorTests.cs b/SW.Bitween.HttpTests/AdapterEditorTests.cs new file mode 100644 index 00000000..b857225a --- /dev/null +++ b/SW.Bitween.HttpTests/AdapterEditorTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The adapter editor over HTTP, as the UI calls it: start a Python adapter, check it with a setting, +/// try it, publish it and make it current - and a Member refused all of it. +/// +[Collection("Http")] +public class AdapterEditorTests(HttpFixture fixture) +{ + [Fact] + public async Task An_admin_writes_checks_tries_publishes_and_promotes_a_python_adapter() + { + using var admin = await fixture.AdminAsync(); + var name = "HttpPy" + Guid.NewGuid().ToString("N")[..8]; + var id = (int)await Api.Json(await admin.PostAsJsonAsync("/api/adapterdrafts", new { name, language = "python", kind = "handler" })); + + var draft = await Api.Json(await admin.GetAsync($"/api/adapterdrafts/{id}")); + var adapterId = (string)draft["adapterId"]!; + Assert.NotNull(draft["files"]!["main.py"]); + + var settings = new { ApiKey = "k" }; + var built = await Api.Json(await admin.PostAsJsonAsync($"/api/adapterdrafts/{id}/build", new { settings })); + Assert.True((bool)built["conforms"]!, built.ToJsonString()); + + var tried = await Api.Json(await admin.PostAsJsonAsync($"/api/adapterdrafts/{id}/try", + new { settings, command = "Handle", input = "{\"Data\":\"ping\"}" })); + Assert.True((bool)tried["succeeded"]!, tried.ToJsonString()); + Assert.Contains("ping", (string)tried["output"]!); + + var published = await Api.Json(await admin.PostAsJsonAsync($"/api/adapterdrafts/{id}/publish", + new { version = "1.0.0", releaseNotes = "From HTTP", settings })); + Assert.True((bool)published["published"]!, published.ToJsonString()); + + await Api.Json(await admin.PostAsJsonAsync("/api/adapters/promote", new { adapterId, version = "1.0.0" })); + var catalog = await Api.Json(await admin.GetAsync("/api/adapters/Catalog?prefix=handlers")); + var row = catalog.AsArray().Single(a => (string)a!["key"]! == adapterId)!; + Assert.Equal("1.0.0", (string)row["currentVersion"]!); + Assert.Equal("python", (string)row["versionHistory"]![0]!["runtime"]!); + + var trail = await Api.Json(await admin.GetAsync("/api/audit?entityName=AdapterRelease&limit=200")); + Assert.Contains(trail["result"]!.AsArray(), r => + (string?)r!["changes"]?["AdapterId"]?["new"] == adapterId && (string?)r["changes"]?["Action"]?["new"] == "promoted"); + } + + [Fact] + public async Task A_member_is_refused_the_editor() + { + using var admin = await fixture.AdminAsync(); + var email = $"ed-{Guid.NewGuid():N}@example.com"[..40]; + const string password = "Editor-Refused-2026!"; + await Api.CreateAsync(admin, "/api/accounts", new { name = "Member", email, password, roleIds = new[] { 2 } }); + using var member = fixture.Client(); + member.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await HttpFixture.SignInAsync(member, email, password)); + + Assert.Equal(403, (int)(await member.GetAsync("/api/adapterdrafts")).StatusCode); + Assert.Equal(403, (int)(await member.PostAsJsonAsync("/api/adapterdrafts", new { name = "Nope", language = "python", kind = "handler" })).StatusCode); + Assert.Equal(403, (int)(await member.PostAsJsonAsync("/api/adapters/promote", new { adapterId = "x", version = "1.0.0" })).StatusCode); + } +} diff --git a/SW.Bitween.HttpTests/AdapterSourceTests.cs b/SW.Bitween.HttpTests/AdapterSourceTests.cs new file mode 100644 index 00000000..84610bed --- /dev/null +++ b/SW.Bitween.HttpTests/AdapterSourceTests.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; +using SW.Serverless; +using SW.Serverless.Contract.Catalog; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// An adapter version's source over HTTP: the list and a file at their routes, the read in the +/// audit trail under the reader's name, and the permission - refused to Members, grantable to a +/// custom role. +/// +[Collection("Http")] +public class AdapterSourceTests(HttpFixture fixture) +{ + const string Handler = "def handle(xchange):\n return xchange\n"; + + async Task PublishAsync() + { + var id = $"http.handlers.source-{Guid.NewGuid():N}"[..36]; + var bytes = Encoding.UTF8.GetBytes(Handler); + var manifest = new AdapterManifest + { + Id = id, Version = "1.0.0", Kinds = ["handler"], Entry = "main.py", Language = "python", Runtime = AdapterManifest.PythonRuntime, + Source = new AdapterSource + { + BuildCommand = "serverless build", + Files = { ["adapter/main.py"] = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant() } + } + }; + + using var zip = new MemoryStream(); + using (var archive = new ZipArchive(zip, ZipArchiveMode.Create, leaveOpen: true)) + { + await using (var s = archive.CreateEntry(AdapterManifest.FileName).Open()) await s.WriteAsync(Encoding.UTF8.GetBytes(manifest.ToJson())); + await using (var s = archive.CreateEntry("main.py").Open()) await s.WriteAsync(bytes); + await using (var s = archive.CreateEntry("source/adapter/main.py").Open()) await s.WriteAsync(bytes); + } + + using var scope = fixture.App.Services.CreateScope(); + var files = scope.ServiceProvider.GetRequiredService(); + var root = scope.ServiceProvider.GetRequiredService().AdapterRemotePath; + zip.Position = 0; + await files.WriteAsync(zip, new WriteFileSettings { Key = AdapterCatalogPaths.Version(root, id, "1.0.0"), ContentType = "application/zip" }); + await new AdapterCatalogStore(files, root).SaveAsync(new AdapterCatalogEntry + { + Id = id, Current = "1.0.0", Manifest = manifest, + Versions = [new AdapterVersionRecord { Version = "1.0.0", PublishedOn = DateTimeOffset.UtcNow, Manifest = manifest }] + }); + scope.ServiceProvider.GetRequiredService().Forget(id); + return id; + } + + async Task MemberWithRolesAsync(HttpClient admin, params int[] roleIds) + { + var email = $"src-{Guid.NewGuid():N}@example.com"[..40]; + const string password = "Source-Reader-2026!"; + await Api.CreateAsync(admin, "/api/accounts", new { name = "Source reader", email, password, roleIds }); + var client = fixture.Client(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", await HttpFixture.SignInAsync(client, email, password)); + return client; + } + + [Fact] + public async Task An_admin_lists_and_reads_a_version_s_source_and_the_read_is_audited() + { + var id = await PublishAsync(); + using var admin = await fixture.AdminAsync(); + + var listing = await Api.Json(await admin.GetAsync($"/api/adapters/source?adapterId={id}&version=1.0.0")); + Assert.Equal("python", (string)listing["language"]!); + Assert.Equal(["adapter/main.py"], listing["files"]!.AsArray().Select(f => (string)f!["path"]!).ToArray()); + + var file = await Api.Json(await admin.GetAsync($"/api/adapters/sourcefile?adapterId={id}&version=1.0.0&path={Uri.EscapeDataString("adapter/main.py")}")); + Assert.Equal(Handler, (string)file["content"]!); + Assert.False((bool)file["binary"]!); + + // The catalog says which versions carry source, for the client to offer it. + var catalog = await Api.Json(await admin.GetAsync("/api/adapters/Catalog?prefix=handlers")); + var row = catalog.AsArray().Single(a => (string)a!["key"]! == id)!; + Assert.True((bool)row["versionHistory"]![0]!["hasSource"]!); + + var trail = await Api.Json(await admin.GetAsync("/api/audit?entityName=AdapterSourceAccess&limit=200")); + Assert.Contains(trail["result"]!.AsArray(), r => + (string?)r!["changes"]?["AdapterId"]?["new"] == id && + (string?)r["changes"]?["Path"]?["new"] == "adapter/main.py" && + !string.IsNullOrEmpty((string?)r["userDisplayName"])); + } + + [Fact] + public async Task Unknown_versions_and_files_are_not_found() + { + var id = await PublishAsync(); + using var admin = await fixture.AdminAsync(); + + Assert.Equal(404, (int)(await admin.GetAsync($"/api/adapters/source?adapterId={id}&version=9.9.9")).StatusCode); + Assert.Equal(404, (int)(await admin.GetAsync($"/api/adapters/sourcefile?adapterId={id}&version=1.0.0&path=main.py")).StatusCode); + Assert.Equal(404, (int)(await admin.GetAsync($"/api/adapters/sourcefile?adapterId={id}&version=1.0.0&path=..%2Fmain.py")).StatusCode); + } + + [Fact] + public async Task Members_are_refused_and_a_custom_role_can_be_granted_it() + { + var id = await PublishAsync(); + using var admin = await fixture.AdminAsync(); + + using var member = await MemberWithRolesAsync(admin, 2); + Assert.Equal(403, (int)(await member.GetAsync($"/api/adapters/source?adapterId={id}&version=1.0.0")).StatusCode); + + var roleId = await Api.CreateAsync(admin, "/api/roles", new + { + name = Api.Unique("Source reader"), description = "reads adapter source", permissions = new[] { "adapter-source.view" } + }); + using var reader = await MemberWithRolesAsync(admin, roleId); + var listing = await Api.Json(await reader.GetAsync($"/api/adapters/source?adapterId={id}&version=1.0.0")); + Assert.Single(listing["files"]!.AsArray()); + } +} diff --git a/SW.Bitween.HttpTests/Api.cs b/SW.Bitween.HttpTests/Api.cs new file mode 100644 index 00000000..591ee801 --- /dev/null +++ b/SW.Bitween.HttpTests/Api.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// Setting things up the way the admin UI does, over the API. +static class Api +{ + static int _seq; + public static string Unique(string prefix) => $"{prefix} {Interlocked.Increment(ref _seq)}-{Guid.NewGuid():N}"[..40]; + + public static async Task Json(HttpResponseMessage response) + { + var text = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode, $"{(int)response.StatusCode}: {text}"); + return string.IsNullOrWhiteSpace(text) ? null! : JsonNode.Parse(text)!; + } + + public static async Task CreateAsync(HttpClient client, string path, object body) => + (int)(await Json(await client.PostAsJsonAsync(path, body))); + + /// Saves the partner's keys the way the partner page does: the whole record, keys replaced. + public static async Task SetKeysAsync(HttpClient admin, int partnerId, params (string Name, string Value)[] keys) + { + var partner = await Json(await admin.GetAsync($"/api/partners/{partnerId}")); + var save = await admin.PostAsJsonAsync($"/api/partners/{partnerId}", new + { + name = (string)partner["name"]!, + adapterProperties = partner["adapterProperties"], + secretProperties = partner["secretProperties"], + loginIdentity = (string?)partner["loginIdentity"], + apiCredentials = keys.Select(k => new { key = k.Name, value = k.Value }) + }); + await Json(save); + } + + /// A gateway with one partner attached through a gateway subscription. + public static async Task<(HttpClient Admin, string UrlName, int GatewayId, int PartnerId, int SubscriptionId)> GatewayAsync(HttpFixture fixture, int? onDocument = null) => + await GatewayAsync(await fixture.AdminAsync(), onDocument); + + public static async Task<(HttpClient Admin, string UrlName, int GatewayId, int PartnerId, int SubscriptionId)> GatewayAsync(HttpClient admin, int? onDocument = null) + { + var documentId = onDocument ?? await CreateAsync(admin, "/api/documents", new { name = Unique("Order"), documentFormat = "Json" }); + var partnerId = await CreateAsync(admin, "/api/partners", new { name = Unique("Partner") }); + var subscriptionId = await CreateAsync(admin, "/api/subscriptions", new + { + name = Unique("Gateway sub"), documentId, type = "GatewayApiCall" + }); + // Created switched off, as every new subscription is; switched on the way the page's + // Enable does it - the whole record saved back with inactive false. + var subscription = (await Json(await admin.GetAsync($"/api/subscriptions/{subscriptionId}"))).AsObject(); + subscription["inactive"] = false; + await Json(await admin.PostAsJsonAsync($"/api/subscriptions/{subscriptionId}", subscription)); + + var urlName = $"orders-{Guid.NewGuid():N}"[..20]; + var gatewayId = await CreateAsync(admin, "/api/apigateways", new { name = Unique("Gateway"), urlName, inactive = false }); + await Json(await admin.PostAsJsonAsync($"/api/apigateways/{gatewayId}/addpartner", new { partnerId, subscriptionId })); + return (admin, urlName, gatewayId, partnerId, subscriptionId); + } + + /// A partner key saved on the partner, and the gateway call it makes, answered once processed. + public static Task CallSyncAsync(HttpFixture fixture, string urlName, string key, string body = "{\"order\":1}") => + CallSyncAsync(fixture.Client(), urlName, key, body); + + /// The call made with , which it disposes. + public static async Task CallSyncAsync(HttpClient partner, string urlName, string key, string body = "{\"order\":1}") + { + using var _ = partner; + using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/gateway/{urlName}/sync") + { + Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json") + }; + request.Headers.Add("partnerkey", key); + request.Headers.Add("Wait-Period", "30"); + return await partner.SendAsync(request); + } + + /// A new key, as the partner page asks for one. Answered as plain text. + public static async Task NewKeyAsync(HttpClient admin) + { + var response = await admin.GetAsync("/api/partners/generatekey"); + Assert.True(response.IsSuccessStatusCode); + return (await response.Content.ReadAsStringAsync()).Trim('"'); + } + +} diff --git a/SW.Bitween.HttpTests/ApplyTests.cs b/SW.Bitween.HttpTests/ApplyTests.cs new file mode 100644 index 00000000..1728eac8 --- /dev/null +++ b/SW.Bitween.HttpTests/ApplyTests.cs @@ -0,0 +1,474 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using SW.Bitween.Cli; +using SW.Bitween.Cli.Configuration; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// bitween plan, apply and export against a Bitween: a configuration is created, a second apply +/// changes nothing, an edit changes exactly what was edited, an export reads back as the same +/// configuration, prune removes only what apply made, and a service account's token can do it all +/// from the environment alone. +/// +[Collection("Http")] +public class ApplyTests(HttpFixture fixture) : IDisposable +{ + static readonly SemaphoreSlim OneAtATime = new(1, 1); + readonly string work = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "bitween-apply-tests", Guid.NewGuid().ToString("N"))).FullName; + readonly string s = Guid.NewGuid().ToString("N")[..8]; + + public void Dispose() + { + try { Directory.Delete(work, true); } catch { } + } + + sealed class FromAddress(HttpMessageHandler inner) : DelegatingHandler(inner) + { + readonly string address = $"10.{Random.Shared.Next(255)}.{Random.Shared.Next(255)}.{Random.Shared.Next(1, 255)}"; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Add("X-Forwarded-For", address); + return base.SendAsync(request, cancellationToken); + } + } + + async Task SignedInAsync() + { + var profiles = new Profiles(Path.Combine(work, "cli.json")); + var (exit, output) = await Bitween(profiles, HttpFixture.AdminPassword + "\n", + "login", "https://localhost", "--email", HttpFixture.AdminEmail, "--password-stdin"); + Assert.True(exit == 0, output); + return profiles; + } + + async Task<(int Exit, string Output)> Bitween(Profiles profiles, string stdin, params string[] args) + { + await OneAtATime.WaitAsync(); + var original = Console.Out; + using var output = new StringWriter(); + var synchronized = TextWriter.Synchronized(output); + Console.SetOut(synchronized); + BitweenApi.HandlerOverride = () => new FromAddress(fixture.App.Server.CreateHandler()); + try + { + var exit = await Program.RunAsync(args, profiles, new StringReader(stdin ?? ""), () => ""); + string text; + lock (synchronized) text = output.ToString(); + return (exit, string.Join("\n", text.Split('\n').Where(l => !l.TrimStart().StartsWith("{\"@t\"")))); + } + finally + { + BitweenApi.HandlerOverride = null; + Console.SetOut(original); + OneAtATime.Release(); + } + } + + string Write(string yaml) + { + var file = Path.Combine(work, "config.yaml"); + File.WriteAllText(file, yaml); + return file; + } + + string Config(string handlerUrl = "https://warehouse.example.com/orders", string partnerKey = "${secret:LAB_PARTNER_KEY}", + bool withOldPartner = true, string extraPartnerProperty = "") => $$""" + apiVersion: bitween/v1 + kind: Configuration + metadata: + name: lab-{{s}} + subscriptionCategories: + - code: LAB_{{s.ToUpperInvariant()}} + description: Lab flows + workGroups: + - name: Lab lane {{s}} + busMessageName: lab-lane-{{s}} + informationTypes: + - name: Marketplace order {{s}} + code: MKT_{{s.ToUpperInvariant()}} + format: Json + promotedProperties: + orderId: $.id + partners: + - name: Acme marketplace {{s}} + properties: + region: eu{{extraPartnerProperty}} + keys: + main: {{partnerKey}} + {{(withOldPartner ? $" - name: Old partner {s}" : "")}} + globalValues: + - id: lab-{{s}} + name: Lab values + values: + warehouse: https://warehouse.example.com + retryPolicies: + - name: Partner outage {{s}} + groups: + - name: server errors + priority: 1 + appliesTo: [Error] + matchers: + - type: contains + value: "503" + budget: + maxAttemptsPerError: 3 + delayStrategy: + type: fixed + delayMs: 1000 + subscriptions: + - name: Orders to warehouse {{s}} + type: GatewayApiCall + informationType: Marketplace order {{s}} + category: LAB_{{s.ToUpperInvariant()}} + workGroup: Lab lane {{s}} + retryPolicy: Partner outage {{s}} + handler: + adapter: NativeHttpHandler + properties: + Url: {{handlerUrl}} + Verb: post + AuthType: ApiKey + ApiKey: ${secret:LAB_WAREHOUSE_KEY} + apiGateways: + - name: Orders inbound {{s}} + urlName: lab-orders-{{s}} + partners: + - partner: Acme marketplace {{s}} + subscription: Orders to warehouse {{s}} + notifiers: + - name: Ops on failure {{s}} + handler: NativeHttpHandler + properties: + Url: https://ops.example.com/alerts + runOnFailedResult: true + subscriptions: [Orders to warehouse {{s}}] + """; + + async Task SubscriptionAsync(HttpClient admin, string name) + { + var rows = (await Api.Json(await admin.GetAsync("/api/subscriptions?page=0&size=1000")))["result"]!.AsArray(); + var id = (int)rows.Single(r => (string)r!["name"]! == name)!["id"]!; + return (await Api.Json(await admin.GetAsync($"/api/subscriptions/{id}"))).AsObject(); + } + + [Fact] + public async Task A_configuration_is_applied_once_and_a_second_apply_changes_nothing() + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", "partner-key-" + s + "-0123456789"); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", "warehouse-secret-" + s); + try + { + var profiles = await SignedInAsync(); + var file = Write(Config()); + + var (exit, output) = await Bitween(profiles, null, "plan", "-f", file, "--detailed-exitcode"); + Assert.True(exit == Commands.Changes, output); + Assert.Contains($"+ subscription \"Orders to warehouse {s}\"", output); + // A secret is never printed, not even in a plan. + Assert.DoesNotContain("warehouse-secret-", output); + + (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + Assert.DoesNotContain("warehouse-secret-", output); + + (exit, output) = await Bitween(profiles, null, "plan", "-f", file, "--detailed-exitcode"); + Assert.True(exit == 0, output); + Assert.Contains("No changes", output); + + // What it made is what the file says, wired together. + using var admin = await fixture.AdminAsync(); + var subscription = await SubscriptionAsync(admin, $"Orders to warehouse {s}"); + Assert.False((bool)subscription["inactive"]!); + Assert.Equal("NativeHttpHandler", (string)subscription["handlerId"]!); + var gateways = (await Api.Json(await admin.GetAsync("/api/apigateways?page=0&size=1000")))["result"]!.AsArray(); + var gatewayId = (int)gateways.Single(g => (string)g!["urlName"]! == $"lab-orders-{s}")!["id"]!; + var gateway = await Api.Json(await admin.GetAsync($"/api/apigateways/{gatewayId}")); + Assert.Equal($"Orders to warehouse {s}", (string)gateway["partners"]![0]!["subscriptionName"]!); + // The partner's key works at the gateway it was attached to. + var call = await Api.CallSyncAsync(fixture, $"lab-orders-{s}", "partner-key-" + s + "-0123456789"); + Assert.NotEqual(System.Net.HttpStatusCode.Unauthorized, call.StatusCode); + + // An edit changes exactly what was edited. + file = Write(Config(handlerUrl: "https://warehouse.example.com/v2/orders", extraPartnerProperty: "-west")); + (exit, output) = await Bitween(profiles, null, "plan", "-f", file); + Assert.True(exit == 0, output); + Assert.Contains($"~ subscription \"Orders to warehouse {s}\"", output); + Assert.Contains("handler:", output); + Assert.Contains($"~ partner \"Acme marketplace {s}\"", output); + Assert.Contains("0 to create, 2 to update", output); + (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + subscription = await SubscriptionAsync(admin, $"Orders to warehouse {s}"); + Assert.Contains("v2/orders", subscription["handlerProperties"]!.ToJsonString()); + // The secret it didn't change is still the one it had. + (exit, output) = await Bitween(profiles, null, "plan", "-f", file, "--detailed-exitcode"); + Assert.True(exit == 0, output); + + // A key that no longer starts as the stored one did is replaced. + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", "rotated-key-" + s + "-0123456789"); + (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + Assert.Contains($"~ partner \"Acme marketplace {s}\"", output); + call = await Api.CallSyncAsync(fixture, $"lab-orders-{s}", "partner-key-" + s + "-0123456789"); + Assert.Equal(System.Net.HttpStatusCode.Unauthorized, call.StatusCode); + call = await Api.CallSyncAsync(fixture, $"lab-orders-{s}", "rotated-key-" + s + "-0123456789"); + Assert.NotEqual(System.Net.HttpStatusCode.Unauthorized, call.StatusCode); + } + finally + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", null); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", null); + } + } + + [Fact] + public async Task Prune_removes_only_what_apply_made_and_the_file_dropped() + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", "partner-key-" + s + "-0123456789"); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", "warehouse-secret-" + s); + try + { + var profiles = await SignedInAsync(); + Assert.Equal(0, (await Bitween(profiles, null, "apply", "-f", Write(Config()))).Exit); + + // Made by hand, alongside: apply never touches it. + using var admin = await fixture.AdminAsync(); + await Api.CreateAsync(admin, "/api/partners", new { name = $"Hand made {s}" }); + + var file = Write(Config(withOldPartner: false)); + var (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + Assert.Contains("--prune removes them", output); + + (exit, output) = await Bitween(profiles, null, "apply", "-f", file, "--prune"); + Assert.True(exit == 0, output); + Assert.Contains($"- partner \"Old partner {s}\"", output); + Assert.DoesNotContain("Hand made", output); + var partners = (await Api.Json(await admin.GetAsync("/api/partners?page=0&size=5000")))["result"]!.ToJsonString(); + Assert.DoesNotContain($"Old partner {s}", partners); + Assert.Contains($"Hand made {s}", partners); + } + finally + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", null); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", null); + } + } + + [Fact] + public async Task An_export_reads_back_as_the_same_configuration() + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", "partner-key-" + s + "-0123456789"); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", "warehouse-secret-" + s); + try + { + var profiles = await SignedInAsync(); + Assert.Equal(0, (await Bitween(profiles, null, "apply", "-f", Write(Config()))).Exit); + + var exported = Path.Combine(work, "exported.yaml"); + var (exit, output) = await Bitween(profiles, null, "export", "-o", exported, "--name", $"lab-{s}"); + Assert.True(exit == 0, output); + var all = Yaml.Read(File.ReadAllText(exported)).Single()!.AsObject(); + + // Only this test's items, from a Bitween other tests share: and as the export wrote them. + var mine = new JsonObject { ["apiVersion"] = "bitween/v1", ["kind"] = "Configuration", ["metadata"] = new JsonObject { ["name"] = $"export-{s}" } }; + foreach (var (section, items) in all.Where(p => p.Value is JsonArray)) + mine[section] = new JsonArray(items!.AsArray().Where(i => i!.ToJsonString().Contains(s)).Select(i => i!.DeepClone()).ToArray()); + var subscription = mine["subscriptions"]!.AsArray().Single()!; + // The secrets it can't show are named, to be supplied. + var apiKey = (string)subscription["handler"]!["properties"]!["ApiKey"]!; + Assert.StartsWith("${secret:", apiKey); + var partnerKey = (string)mine["partners"]!.AsArray().First(p => ((string)p!["name"]!).StartsWith("Acme"))!["keys"]!["main"]!; + Assert.StartsWith("${secret:", partnerKey); + Environment.SetEnvironmentVariable(apiKey[9..^1], "warehouse-secret-" + s); + Environment.SetEnvironmentVariable(partnerKey[9..^1], "partner-key-" + s + "-0123456789"); + + (exit, output) = await Bitween(profiles, null, "plan", "-f", Write(Yaml.Write(mine)), "--detailed-exitcode"); + Assert.True(exit == 0, output); + Assert.Contains("No changes", output); + } + finally + { + Environment.SetEnvironmentVariable("LAB_PARTNER_KEY", null); + Environment.SetEnvironmentVariable("LAB_WAREHOUSE_KEY", null); + } + } + + [Fact] + public async Task Adapters_data_sources_bus_routes_responses_and_pausing_apply_and_settle() + { + var profiles = await SignedInAsync(); + var name = "LabPy" + s; + var id = SW.Serverless.Tooling.Scaffolding.Scaffolder.IdFrom(name); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "init", name, "--kind", "handler", "--lang", "python", "--dir", work)).Exit); + var build = await Bitween(profiles, null, "adapter", "build", Path.Combine(work, name)); + Assert.True(build.Exit == 0, build.Output); + File.Copy(Path.Combine(work, name, "bin", "serverless", $"{id}-0.1.0.zip"), Path.Combine(work, "handler.zip")); + Environment.SetEnvironmentVariable("LAB_DB_PASSWORD", "db-password-" + s); + + string Config(bool paused) => $""" + apiVersion: bitween/v1 + kind: Configuration + metadata: + name: wiring-{s} + adapters: + - package: handler.zip + current: true + informationTypes: + - name: Stock update {s} + format: Json + busEnabled: true + busMessageTypeName: LabStock{s} + partners: + - name: Shop {s} + dataSources: + - name: ERP {s} + adapter: sw.bitween.adapters.db.postgresql + kind: Relational + inactive: true + properties: + Host: erp.example.com + Password: ${"{"}secret:LAB_DB_PASSWORD{"}"} + dataSourceStatements: + - dataSource: ERP {s} + name: new orders + sql: select * from orders where id > :cursor + cursorColumn: id + subscriptions: + - name: Stock to shop {s} + type: ApiCall + informationType: Stock update {s} + partner: Shop {s} + responseSubscription: Shop answers {s} + paused: {(paused ? "true" : "false")} + handler: + adapter: {id} + version: 0.1.0 + properties: + ApiKey: k + - name: Shop answers {s} + type: Response + informationType: Stock update {s} + - name: Stock from bus {s} + type: BusGateway + informationType: Stock update {s} + busGateways: + - name: Stock bus {s} + informationType: Stock update {s} + routes: + - subscription: Stock from bus {s} + partner: Shop {s} + matchExpression: + type: one_of + path: $.warehouse + values: [north] + """; + try + { + var file = Write(Config(paused: true)); + var (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + Assert.Contains($"+ publish adapter {id} 0.1.0, current", output); + // The response subscription is made before the one that answers to it, though listed after. + Assert.True(output.IndexOf($"Shop answers {s}", StringComparison.Ordinal) < output.IndexOf($"Stock to shop {s}", StringComparison.Ordinal), output); + + (exit, output) = await Bitween(profiles, null, "plan", "-f", file, "--detailed-exitcode"); + Assert.True(exit == 0, output); + // The same package again: Bitween re-zips what it takes, so its hash isn't this file's, and that isn't a difference. + Assert.DoesNotContain("different package", output); + + using var admin = await fixture.AdminAsync(); + var rows = (await Api.Json(await admin.GetAsync("/api/subscriptions?page=0&size=2000")))["result"]!.AsArray(); + var stock = rows.Single(r => (string)r!["name"]! == $"Stock to shop {s}")!; + Assert.NotNull((string?)stock["pausedOn"]); + Assert.Equal("0.1.0", (string)stock["handlerVersion"]!); + + (exit, output) = await Bitween(profiles, null, "apply", "-f", Write(Config(paused: false))); + Assert.True(exit == 0, output); + Assert.Contains("paused: true -> false", output); + rows = (await Api.Json(await admin.GetAsync("/api/subscriptions?page=0&size=2000")))["result"]!.AsArray(); + Assert.Null((string?)rows.Single(r => (string)r!["name"]! == $"Stock to shop {s}")!["pausedOn"]); + } + finally + { + Environment.SetEnvironmentVariable("LAB_DB_PASSWORD", null); + } + } + + [Fact] + public async Task A_service_token_in_the_environment_is_enough_to_apply() + { + using var admin = await fixture.AdminAsync(); + var accountId = await Api.CreateAsync(admin, "/api/serviceaccounts", new { name = $"lab apply {s}", roleIds = new[] { 1 } }); + var token = (string)(await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{accountId}/issueToken", new { name = "job" })))["token"]!; + + var file = Write($""" + apiVersion: bitween/v1 + kind: Configuration + metadata: + name: token-{s} + informationTypes: + - name: Token made {s} + format: Json + """); + // No profile anywhere: what a job in a container has. + var noProfiles = new Profiles(Path.Combine(work, "none.json")); + Environment.SetEnvironmentVariable("BITWEEN_URL", "https://localhost"); + Environment.SetEnvironmentVariable("BITWEEN_TOKEN", token); + try + { + var (exit, output) = await Bitween(noProfiles, null, "apply", "-f", file); + Assert.True(exit == 0, output); + Assert.Contains($"+ information type \"Token made {s}\"", output); + (exit, output) = await Bitween(noProfiles, null, "whoami"); + Assert.Contains("service account", output); + + Environment.SetEnvironmentVariable("BITWEEN_TOKEN", "bwt_" + new string('n', 40)); + (exit, output) = await Bitween(noProfiles, null, "plan", "-f", file); + Assert.Equal(Commands.Failure, exit); + Assert.Contains("refused the service token", output); + } + finally + { + Environment.SetEnvironmentVariable("BITWEEN_URL", null); + Environment.SetEnvironmentVariable("BITWEEN_TOKEN", null); + } + Assert.False(File.Exists(Path.Combine(work, "none.json"))); + } + + [Fact] + public async Task A_file_that_names_what_isnt_there_or_changes_what_cant_change_is_refused_before_anything_changes() + { + var profiles = await SignedInAsync(); + var file = Write($""" + apiVersion: bitween/v1 + kind: Configuration + metadata: + name: broken-{s} + informationTypes: + - name: Refused {s} + format: Json + subscriptions: + - name: Nowhere {s} + type: ApiCall + informationType: Refused {s} + partner: Nobody {s} + """); + var (exit, output) = await Bitween(profiles, null, "apply", "-f", file); + Assert.Equal(Commands.Failure, exit); + Assert.Contains($"there's no partner \"Nobody {s}\"", output); + using var admin = await fixture.AdminAsync(); + Assert.DoesNotContain($"Refused {s}", (await Api.Json(await admin.GetAsync("/api/documents?page=0&size=5000"))).ToJsonString()); + + Assert.Equal(1, (await Bitween(profiles, null, "plan", "-f", Write("apiVersion: bitween/v1\nkind: Configuration\nmetadata: { name: x }\nwidgets: []\n"))).Exit); + } +} diff --git a/SW.Bitween.HttpTests/AuditTests.cs b/SW.Bitween.HttpTests/AuditTests.cs new file mode 100644 index 00000000..48b97a24 --- /dev/null +++ b/SW.Bitween.HttpTests/AuditTests.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The audit trail over HTTP: changes made through the API are recorded against the record and the +/// person who made them, with what changed, and the timeline for one record shows only that record. +/// +[Collection("Http")] +public class AuditTests(HttpFixture fixture) +{ + [Fact] + public async Task A_partner_s_timeline_shows_who_created_and_renamed_it_and_what_changed() + { + using var admin = await fixture.AdminAsync(); + var original = Api.Unique("Audited"); + var partnerId = await Api.CreateAsync(admin, "/api/partners", new { name = original }); + var renamed = Api.Unique("Renamed"); + await Api.Json(await admin.PostAsJsonAsync($"/api/partners/{partnerId}", new { name = renamed })); + // Another partner's history must not leak into this one's timeline. + await Api.CreateAsync(admin, "/api/partners", new { name = Api.Unique("Bystander") }); + + var timeline = await Api.Json(await admin.GetAsync($"/api/audit?entityName=Partner&entityKey={partnerId}")); + var rows = timeline["result"]!.AsArray(); + + Assert.Equal(rows.Count, (int)timeline["totalCount"]!); + Assert.All(rows, r => Assert.Equal(partnerId.ToString(), (string)r!["entityKey"]!)); + Assert.Equal(["Modified", "Added"], rows.Select(r => (string)r!["state"]!).ToArray()); + + var rename = rows[0]!["changes"]!["Name"]!; + Assert.Equal(original, (string)rename["old"]!); + Assert.Equal(renamed, (string)rename["new"]!); + Assert.All(rows, r => Assert.False(string.IsNullOrEmpty((string?)r!["userDisplayName"]), "who made the change")); + } +} diff --git a/SW.Bitween.HttpTests/BackgroundTests.cs b/SW.Bitween.HttpTests/BackgroundTests.cs new file mode 100644 index 00000000..90c9850e --- /dev/null +++ b/SW.Bitween.HttpTests/BackgroundTests.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// GET /api/ops/background: the system jobs, scheduled at startup, and the outbox. +[Collection("Http")] +public class BackgroundTests(HttpFixture fixture) +{ + [Fact] + public async Task The_system_jobs_are_scheduled_and_say_when_they_run_next() + { + using var admin = await fixture.AdminAsync(); + System.Text.Json.Nodes.JsonNode body = null; + // Startup schedules them in the background; give it a moment on a busy machine. + for (var i = 0; i < 40; i++) + { + body = await Api.Json(await admin.GetAsync("/api/ops/background")); + if (body["jobs"]!.AsArray().All(j => (bool)j!["scheduled"]!)) break; + await Task.Delay(250); + } + + var jobs = body!["jobs"]!.AsArray(); + Assert.Equal(["Automatic retries", "Exchange retention", "Receive attempt clean-up", "Broker deduplication clean-up", + "Queue snapshots", "Metrics clean-up"], + jobs.Select(j => (string)j!["name"]).ToArray()); + Assert.All(jobs, j => + { + Assert.True((bool)j!["scheduled"]!, (string)j["name"]); + Assert.NotNull(j["nextRunOn"]); + Assert.False(string.IsNullOrEmpty((string)j["cron"])); + }); + + var outbox = body["outbox"]!; + Assert.True((int)outbox["pending"]! >= 0); + Assert.True((int)outbox["publishedLastHour"]! >= 0); + } +} diff --git a/SW.Bitween.HttpTests/BitweenCliTests.cs b/SW.Bitween.HttpTests/BitweenCliTests.cs new file mode 100644 index 00000000..56e08f59 --- /dev/null +++ b/SW.Bitween.HttpTests/BitweenCliTests.cs @@ -0,0 +1,298 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Cli; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The bitween CLI against a Bitween, through its real entry point: sign in, write a Python adapter, +/// check it against the Bitween contract, publish it through the API, list, promote and withdraw its +/// versions - and a Member who signs in refused the publishing. +/// +[Collection("Http")] +public class BitweenCliTests(HttpFixture fixture) : IDisposable +{ + static readonly SemaphoreSlim OneAtATime = new(1, 1); + readonly string work = Path.Combine(Path.GetTempPath(), "bitween-cli-tests", Guid.NewGuid().ToString("N")); + + /// The test server's own handler, from an address of its own so sign-in limits don't leak between tests. + sealed class FromAddress(HttpMessageHandler inner) : DelegatingHandler(inner) + { + readonly string address = $"10.{Random.Shared.Next(255)}.{Random.Shared.Next(255)}.{Random.Shared.Next(1, 255)}"; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Add("X-Forwarded-For", address); + return base.SendAsync(request, cancellationToken); + } + } + + async Task<(int Exit, string Output)> Bitween(Profiles profiles, string stdin, params string[] args) + { + await OneAtATime.WaitAsync(); + var original = Console.Out; + using var output = new StringWriter(); + // The in-process server logs to the console from its own threads while the CLI writes, and + // may still be writing when the output is read; reads take the same lock the writes do. + var synchronized = TextWriter.Synchronized(output); + Console.SetOut(synchronized); + BitweenApi.HandlerOverride = () => new FromAddress(fixture.App.Server.CreateHandler()); + try + { + var exit = await Program.RunAsync(args, profiles, new StringReader(stdin ?? ""), () => ""); + // The in-process server logs to the same console as JSON lines; what the CLI said is the rest. + string text; + lock (synchronized) text = output.ToString(); + var said = string.Join("\n", text.Split('\n').Where(l => !l.TrimStart().StartsWith("{\"@t\""))); + return (exit, said); + } + finally + { + BitweenApi.HandlerOverride = null; + Console.SetOut(original); + OneAtATime.Release(); + } + } + + [Fact] + public async Task An_adapter_goes_from_init_to_published_and_current_through_the_api() + { + var profiles = new Profiles(Path.Combine(work, "cli.json")); + var login = await Bitween(profiles, HttpFixture.AdminPassword + "\n", + "login", "https://localhost", "--email", HttpFixture.AdminEmail, "--password-stdin"); + Assert.True(login.Exit == 0, login.Output); + Assert.Contains("Signed in to https://localhost", login.Output); + + var name = "CliPy" + Guid.NewGuid().ToString("N")[..8]; + var id = SW.Serverless.Tooling.Scaffolding.Scaffolder.IdFrom(name); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "init", name, "--kind", "handler", "--lang", "python", "--dir", work)).Exit); + var project = Path.Combine(work, name); + File.WriteAllText(Path.Combine(work, "settings.json"), """{ "ApiKey": "k" }"""); + + var test = await Bitween(profiles, null, "adapter", "test", project, "--settings", Path.Combine(work, "settings.json")); + Assert.True(test.Exit == 0, test.Output); + Assert.Contains("PASS bitween handler: Handle answers example 1", test.Output); + + var build = await Bitween(profiles, null, "adapter", "build", project); + Assert.True(build.Exit == 0, build.Output); + var zip = Path.Combine(project, "bin", "serverless", $"{id}-0.1.0.zip"); + + var published = await Bitween(profiles, null, "adapter", "publish", zip, "--notes", "From the CLI"); + Assert.True(published.Exit == 0, published.Output); + Assert.Contains($"Published {id} 0.1.0", published.Output); + Assert.Contains("It isn't current", published.Output); + + var current = await Bitween(profiles, null, "adapter", "publish", zip, "-v", "minor", "--current"); + Assert.Contains($"Published {id} 0.2.0", current.Output); + + var versions = await Bitween(profiles, null, "adapter", "versions", id); + Assert.Contains("* 0.2.0", versions.Output); + Assert.Contains(" 0.1.0", versions.Output); + + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "promote", id, "0.1.0")).Exit); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "withdraw", id, "0.2.0")).Exit); + versions = await Bitween(profiles, null, "adapter", "versions", id); + Assert.Contains("* 0.1.0", versions.Output); + Assert.Contains("0.2.0", versions.Output.Split('\n').Single(l => l.Contains("0.2.0"))); + Assert.Contains("withdrawn", versions.Output.Split('\n').Single(l => l.Contains("0.2.0"))); + + // In Bitween, as any published adapter: listed, current at 0.1.0, with its source. + using var admin = await fixture.AdminAsync(); + var catalog = await Api.Json(await admin.GetAsync("/api/adapters/Catalog?prefix=handlers")); + var row = catalog.AsArray().Single(a => (string)a!["key"]! == id)!; + Assert.Equal("0.1.0", (string)row["currentVersion"]!); + Assert.Equal("From the CLI", (string)row["versionHistory"]!.AsArray().Single(v => (string)v!["version"]! == "0.1.0")!["releaseNotes"]!); + Assert.True((bool)row["versionHistory"]![0]!["hasSource"]!); + + var trail = await Api.Json(await admin.GetAsync("/api/audit?entityName=AdapterRelease&limit=200")); + var actions = trail["result"]!.AsArray().Where(r => (string?)r!["changes"]?["AdapterId"]?["new"] == id) + .Select(r => (string)r!["changes"]!["Action"]!["new"]! + " " + (string)r["changes"]!["Version"]!["new"]!).OrderBy(a => a).ToArray(); + Assert.Equal(["promoted 0.1.0", "promoted 0.2.0", "published 0.1.0", "published 0.2.0", "withdrawn 0.2.0"], actions); + + // And it runs, on a node with data sources off - the default: Python adapters run on the + // resident host, which every node has. + using (var scope = fixture.App.Services.CreateScope()) + { + var service = scope.ServiceProvider.GetRequiredService(); + await service.StartAsync(id, "cli-test", new System.Collections.Generic.Dictionary { ["ApiKey"] = "k" }); + try + { + var answer = await service.InvokeAsync("Handle", new { Data = "ping", Filename = "p.txt" }); + Assert.Equal("ping", (string)answer["Data"]); + } + finally + { + ((IDisposable)service).Dispose(); + } + } + + Assert.Equal(0, (await Bitween(profiles, null, "logout")).Exit); + Assert.NotEqual(0, (await Bitween(profiles, null, "adapter", "versions", id)).Exit); + } + + [Fact] + public async Task A_member_can_sign_in_and_build_but_not_publish() + { + using var admin = await fixture.AdminAsync(); + var email = $"cli-{Guid.NewGuid():N}@example.com"[..40]; + const string password = "Cli-Member-2026!"; + await Api.CreateAsync(admin, "/api/accounts", new { name = "Cli member", email, password, roleIds = new[] { 2 } }); + + var profiles = new Profiles(Path.Combine(work, "member.json")); + Assert.Equal(0, (await Bitween(profiles, password + "\n", "login", "https://localhost", "--email", email, "--password-stdin")).Exit); + var whoami = await Bitween(profiles, null, "whoami"); + Assert.Contains("No adapter permissions", whoami.Output); + + var name = "CliRefused" + Guid.NewGuid().ToString("N")[..6]; + await Bitween(profiles, null, "adapter", "init", name, "--lang", "python", "--dir", work); + var build = await Bitween(profiles, null, "adapter", "build", Path.Combine(work, name)); + Assert.True(build.Exit == 0, build.Output); + var zip = Directory.GetFiles(Path.Combine(work, name, "bin", "serverless"), "*.zip").Single(); + + var refused = await Bitween(profiles, null, "adapter", "publish", zip); + Assert.Equal(1, refused.Exit); + Assert.Contains("doesn't have the permission", refused.Output); + } + + [Fact] + public async Task A_wrong_password_is_refused_and_nothing_is_kept() + { + var profiles = new Profiles(Path.Combine(work, "wrong.json")); + var login = await Bitween(profiles, "not-the-password\n", "login", "https://localhost", "--email", HttpFixture.AdminEmail, "--password-stdin"); + Assert.Equal(1, login.Exit); + Assert.Contains("Invalid username or password", login.Output); + Assert.False(File.Exists(profiles.Path)); + } + + /// + /// Plays the browser: signs in to the admin UI as the administrator, confirms the CLI's request + /// on the cli-login page (the grant), and follows the redirect back to the CLI's loopback. + /// + bool ConfirmInBrowser(string address) + { + _ = Task.Run(async () => + { + var query = System.Web.HttpUtility.ParseQueryString(new Uri(address).Query); + using var admin = await fixture.AdminAsync(); + var grant = await admin.PostAsJsonAsync("/api/accounts/cligrant", new { codeChallenge = query["challenge"] }); + var code = (await grant.Content.ReadFromJsonAsync())!["code"]!.ToString(); + using var loopback = new HttpClient(); + await loopback.GetAsync($"http://127.0.0.1:{query["port"]}/callback?code={Uri.EscapeDataString(code)}&state={query["state"]}"); + }); + return true; + } + + [Fact] + public async Task Signing_in_through_the_browser_works_and_logout_ends_the_session_on_the_server() + { + var profiles = new Profiles(Path.Combine(work, "browser.json")); + BrowserSignIn.OpenOverride = ConfirmInBrowser; + try + { + var login = await Bitween(profiles, null, "login", "https://localhost"); + Assert.True(login.Exit == 0, login.Output); + Assert.Contains($"as {HttpFixture.AdminEmail}", login.Output); + } + finally + { + BrowserSignIn.OpenOverride = null; + } + + var whoami = await Bitween(profiles, null, "whoami"); + Assert.True(whoami.Exit == 0, whoami.Output); + Assert.Contains(HttpFixture.AdminEmail, whoami.Output); + + var refreshToken = profiles.Find().Profile.RefreshToken; + Assert.False(string.IsNullOrEmpty(refreshToken)); + + var logout = await Bitween(profiles, null, "logout"); + Assert.True(logout.Exit == 0, logout.Output); + Assert.Contains("Signed out of", logout.Output); + + // The token the CLI held no longer renews a session anywhere. + var renew = await fixture.Client().PostAsJsonAsync("/api/accounts/login", new { refreshToken }); + Assert.False(renew.IsSuccessStatusCode); + } + + [Fact] + public async Task A_sign_in_code_is_refused_without_its_verifier_or_when_altered() + { + var verifier = new string('v', 43); + var challenge = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.ASCII.GetBytes(verifier))) + .TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + using var admin = await fixture.AdminAsync(); + var grant = await admin.PostAsJsonAsync("/api/accounts/cligrant", new { codeChallenge = challenge }); + Assert.True(grant.IsSuccessStatusCode); + var code = (await grant.Content.ReadFromJsonAsync())!["code"]!.ToString(); + + // Not a bearer token: the code can't be used to call the API. + using var withCode = fixture.Client(); + withCode.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", code); + Assert.Equal(401, (int)(await withCode.GetAsync("/api/accounts/profile")).StatusCode); + + using var client = fixture.Client(); + Assert.False((await client.PostAsJsonAsync("/api/accounts/clitoken", new { code, codeVerifier = new string('w', 43) })).IsSuccessStatusCode); + var altered = code[..^2] + (code[^2] == 'A' ? "B" : "A") + code[^1]; + Assert.False((await client.PostAsJsonAsync("/api/accounts/clitoken", new { code = altered, codeVerifier = verifier })).IsSuccessStatusCode); + + var redeemed = await client.PostAsJsonAsync("/api/accounts/clitoken", new { code, codeVerifier = verifier }); + Assert.True(redeemed.IsSuccessStatusCode); + var session = (await redeemed.Content.ReadFromJsonAsync())!; + Assert.Equal(HttpFixture.AdminEmail, session["email"]!.ToString()); + Assert.False(string.IsNullOrEmpty(session["refreshToken"]?.ToString())); + + // Signed out, a grant needs a session. + Assert.Equal(401, (int)(await fixture.Client().PostAsJsonAsync("/api/accounts/cligrant", new { codeChallenge = challenge })).StatusCode); + } + + [Fact] + public async Task Publishing_on_one_node_refreshes_what_the_others_have_cached() + { + var profiles = new Profiles(Path.Combine(work, "nodes.json")); + Assert.Equal(0, (await Bitween(profiles, HttpFixture.AdminPassword + "\n", + "login", "https://localhost", "--email", HttpFixture.AdminEmail, "--password-stdin")).Exit); + + var name = "CliNodes" + Guid.NewGuid().ToString("N")[..8]; + var id = SW.Serverless.Tooling.Scaffolding.Scaffolder.IdFrom(name); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "init", name, "--lang", "python", "--dir", work)).Exit); + var project = Path.Combine(work, name); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "build", project)).Exit); + var zip = Path.Combine(project, "bin", "serverless", $"{id}-0.1.0.zip"); + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "publish", zip, "--current")).Exit); + + await using var second = fixture.SecondNode(); + using var admin = HttpFixture.ClientOf(second); + admin.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", + await HttpFixture.SignInAsync(admin, HttpFixture.AdminEmail, HttpFixture.AdminPassword)); + async Task CurrentOnSecond() => + (string)(await Api.Json(await admin.GetAsync("/api/adapters/Catalog?prefix=handlers"))) + .AsArray().Single(a => (string)a!["key"]! == id)!["currentVersion"]!; + + // Read once on the second node, so it holds the entry in its cache. + Assert.Equal("0.1.0", await CurrentOnSecond()); + + // Published and made current through the first node: the second hears of it, rather than + // showing 0.1.0 until its cache expires a minute later. + Assert.Equal(0, (await Bitween(profiles, null, "adapter", "publish", zip, "-v", "minor", "--current")).Exit); + var seen = ""; + for (var i = 0; i < 40 && seen != "0.2.0"; i++) + { + seen = await CurrentOnSecond(); + if (seen != "0.2.0") await Task.Delay(250); + } + Assert.Equal("0.2.0", seen); + } + + public void Dispose() + { + try { Directory.Delete(work, true); } catch { } + } +} diff --git a/SW.Bitween.HttpTests/ClusterAndMapperTests.cs b/SW.Bitween.HttpTests/ClusterAndMapperTests.cs new file mode 100644 index 00000000..3e96d399 --- /dev/null +++ b/SW.Bitween.HttpTests/ClusterAndMapperTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// What one instance changes, another instance on the same database and broker picks up - by the +/// cache-revoke broadcast over RabbitMQ, not by a restart. And saving a subscription's mapper. +/// +[Collection("Http")] +public class ClusterAndMapperTests(HttpFixture fixture) +{ + [Fact] + public async Task A_setting_changed_on_one_node_reaches_the_other_without_a_restart() + { + await using var second = fixture.SecondNode(); + using var onSecond = HttpFixture.ClientOf(second); + var before = (string?)(await Api.Json(await onSecond.GetAsync("/api/settings/config")))["theme"]!["companyName"]; + + var name = $"Acme {Guid.NewGuid():N}"[..20]; + using var admin = await fixture.AdminAsync(); + await Api.Json(await admin.PostAsJsonAsync("/api/settings/Theme.CompanyName", new { value = name })); + + string? seen = before; + var clock = Stopwatch.StartNew(); + while (seen != name && clock.Elapsed < TimeSpan.FromSeconds(30)) + { + await Task.Delay(250); + seen = (string?)(await Api.Json(await onSecond.GetAsync("/api/settings/config")))["theme"]!["companyName"]; + } + Assert.Equal(name, seen); + } + + [Fact] + public async Task A_subscription_switched_off_on_one_node_stops_taking_calls_on_the_other() + { + await using var second = fixture.SecondNode(); + using var onSecond = HttpFixture.ClientOf(second); + onSecond.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", + await HttpFixture.SignInAsync(onSecond, HttpFixture.AdminEmail, HttpFixture.AdminPassword)); + + using var admin = await fixture.AdminAsync(); + var (_, urlName, _, partnerId, subscriptionId) = await Api.GatewayAsync(fixture); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + + // The second node learns of the gateway, and caches it, by serving a call. + using (var partner = HttpFixture.ClientOf(second)) + { + partner.DefaultRequestHeaders.Add("partnerkey", key); + Assert.Equal(System.Net.HttpStatusCode.Accepted, + (await partner.PostAsync($"/api/gateway/{urlName}/async", JsonContent.Create(new { n = 1 }))).StatusCode); + } + + // Switched off on the first node: the second must stop accepting calls for it. + var subscription = (await Api.Json(await admin.GetAsync($"/api/subscriptions/{subscriptionId}"))).AsObject(); + subscription["inactive"] = true; + await Api.Json(await admin.PostAsJsonAsync($"/api/subscriptions/{subscriptionId}", subscription)); + + var status = System.Net.HttpStatusCode.Accepted; + var clock = Stopwatch.StartNew(); + while (status == System.Net.HttpStatusCode.Accepted && clock.Elapsed < TimeSpan.FromSeconds(30)) + { + await Task.Delay(250); + using var partner = HttpFixture.ClientOf(second); + partner.DefaultRequestHeaders.Add("partnerkey", key); + status = (await partner.PostAsync($"/api/gateway/{urlName}/async", JsonContent.Create(new { n = 2 }))).StatusCode; + } + Assert.NotEqual(System.Net.HttpStatusCode.Accepted, status); + } + + [Fact] + public async Task Saving_a_mapper_stores_it_on_the_subscription() + { + var (admin, _, _, _, subscriptionId) = await Api.GatewayAsync(fixture); + + await Api.Json(await admin.PostAsJsonAsync($"/api/subscriptions/{subscriptionId}/savemapper", new + { + mapperId = "NativeMapper", + mapperProperties = new[] { new { key = "Note", value = "kept" } } + })); + + var saved = await Api.Json(await admin.GetAsync($"/api/subscriptions/{subscriptionId}")); + Assert.Equal("NativeMapper", (string?)saved["mapperId"]); + Assert.Contains("kept", saved["mapperProperties"]!.ToJsonString()); + + var missing = await admin.PostAsJsonAsync("/api/subscriptions/999999/savemapper", new + { + mapperId = "NativeMapper", mapperProperties = Array.Empty() + }); + Assert.Equal(System.Net.HttpStatusCode.NotFound, missing.StatusCode); + admin.Dispose(); + } +} diff --git a/SW.Bitween.HttpTests/ClusterNodesTests.cs b/SW.Bitween.HttpTests/ClusterNodesTests.cs new file mode 100644 index 00000000..b1e7521c --- /dev/null +++ b/SW.Bitween.HttpTests/ClusterNodesTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// GET /api/cluster/nodes: the nodes, as their heartbeats describe them. +[Collection("Http")] +public class ClusterNodesTests(HttpFixture fixture) +{ + [Fact] + public async Task The_node_that_answers_is_listed_online_with_its_runtimes() + { + using var admin = await fixture.AdminAsync(); + System.Text.Json.Nodes.JsonNode body = null; + // The heartbeat writes when the node starts; give it a moment on a busy machine. + for (var i = 0; i < 20; i++) + { + body = await Api.Json(await admin.GetAsync("/api/cluster/nodes")); + if (body["nodes"]!.AsArray().Count > 0) break; + await Task.Delay(250); + } + + var answeredBy = (string)body!["answeredBy"]; + Assert.Equal($"{Environment.MachineName}:{Environment.ProcessId}", answeredBy); + var self = body["nodes"]!.AsArray().Single(n => (string)n!["name"] == answeredBy)!; + Assert.True((bool)self["online"]!); + Assert.Equal(Environment.MachineName, (string)self["host"]); + Assert.Contains("python", self["runtimes"]!.AsArray().Select(r => (string)r)); + Assert.False((bool)self["dataSources"]!); + } + + [Fact] + public async Task Signed_out_it_is_refused() + { + Assert.Equal(HttpStatusCode.Unauthorized, (await fixture.Client().GetAsync("/api/cluster/nodes")).StatusCode); + } +} diff --git a/SW.Bitween.HttpTests/DeleteTests.cs b/SW.Bitween.HttpTests/DeleteTests.cs new file mode 100644 index 00000000..953a79a7 --- /dev/null +++ b/SW.Bitween.HttpTests/DeleteTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The deletes that reach furthest, over HTTP: refused plainly while configuration still depends on +/// the thing, and once allowed, taking with them what belongs to it - exchanges, scheduled retries, +/// the broker queue - and nothing else. +/// +[Collection("Http")] +public class DeleteTests(HttpFixture fixture) +{ + static async Task RefusedAsync(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + Assert.True(response.StatusCode == HttpStatusCode.BadRequest, $"expected a refusal, got {(int)response.StatusCode}: {body}"); + return body; + } + + async Task EventuallyAsync(Func> condition, int seconds = 30) + { + var clock = Stopwatch.StartNew(); + while (clock.Elapsed < TimeSpan.FromSeconds(seconds)) + { + if (await condition()) return true; + await Task.Delay(500); + } + return await condition(); + } + + [Fact] + public async Task Deleting_an_information_type_is_refused_while_used_then_takes_its_exchanges_and_queue() + { + using var admin = await fixture.AdminAsync(); + var messageType = $"OrderPlaced{Guid.NewGuid():N}"[..24]; + var documentId = await Api.CreateAsync(admin, "/api/documents", new + { + name = Api.Unique("Shipment"), documentFormat = "Json", busEnabled = true, busMessageTypeName = messageType + }); + var lane = $".busservice.{messageType.ToLowerInvariant()}"; + Assert.True(await EventuallyAsync(async () => (await fixture.QueuesAsync()).Any(q => q.EndsWith(lane))), + $"no queue for {messageType} after enabling the bus"); + + var (_, urlName, gatewayId, partnerId, subscriptionId) = await Api.GatewayAsync(fixture, documentId); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + var call = await Api.CallSyncAsync(fixture, urlName, key); + Assert.Equal(HttpStatusCode.OK, call.StatusCode); + Assert.Equal(1, await fixture.InDbAsync(db => db.Set().CountAsync(x => x.DocumentId == documentId))); + + Assert.Contains("subscriptions still carry", await RefusedAsync(await admin.DeleteAsync($"/api/documents/{documentId}"))); + + await Api.Json(await admin.PostAsJsonAsync($"/api/apigateways/{gatewayId}/removepartner", new { partnerId })); + await Api.Json(await admin.DeleteAsync($"/api/subscriptions/{subscriptionId}")); + await Api.Json(await admin.DeleteAsync($"/api/documents/{documentId}")); + + Assert.Equal(HttpStatusCode.NotFound, (await admin.GetAsync($"/api/documents/{documentId}")).StatusCode); + Assert.Equal(0, await fixture.InDbAsync(db => db.Set().CountAsync(x => x.DocumentId == documentId))); + Assert.True(await EventuallyAsync(async () => !(await fixture.QueuesAsync()).Any(q => q.Contains(lane))), + $"the queue for {messageType} outlived its information type"); + } + + [Fact] + public async Task The_built_in_aggregation_type_cannot_be_deleted() + { + using var admin = await fixture.AdminAsync(); + Assert.Contains("can't be deleted", + await RefusedAsync(await admin.DeleteAsync($"/api/documents/{Document.AggregationDocumentId}"))); + Assert.True((await admin.GetAsync($"/api/documents/{Document.AggregationDocumentId}")).IsSuccessStatusCode); + } + + [Fact] + public async Task Deleting_a_partner_is_refused_while_a_gateway_holds_it_and_the_system_partner_never() + { + var (admin, _, gatewayId, partnerId, _) = await Api.GatewayAsync(fixture); + var gatewayName = (string)(await Api.Json(await admin.GetAsync($"/api/apigateways/{gatewayId}")))["name"]!; + + var refusal = await RefusedAsync(await admin.DeleteAsync($"/api/partners/{partnerId}")); + Assert.Contains("still used by", refusal); + Assert.Contains(gatewayName, refusal); + + await Api.Json(await admin.PostAsJsonAsync($"/api/apigateways/{gatewayId}/removepartner", new { partnerId })); + await Api.Json(await admin.DeleteAsync($"/api/partners/{partnerId}")); + Assert.Equal(HttpStatusCode.NotFound, (await admin.GetAsync($"/api/partners/{partnerId}")).StatusCode); + + Assert.Contains("can not be deleted", await RefusedAsync(await admin.DeleteAsync($"/api/partners/{Partner.SystemId}"))); + admin.Dispose(); + } +} diff --git a/SW.Bitween.HttpTests/ExchangeSummaryHttpTests.cs b/SW.Bitween.HttpTests/ExchangeSummaryHttpTests.cs new file mode 100644 index 00000000..ff69df40 --- /dev/null +++ b/SW.Bitween.HttpTests/ExchangeSummaryHttpTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The exchange summary by the route the list calls it on, GET /api/xchanges/summary: the +/// named searchy handler is reachable, takes the list's own filter parameters from the query +/// string, and answers in the camel-cased shape the client reads. +/// +[Collection("Http")] +public class ExchangeSummaryHttpTests(HttpFixture fixture) +{ + [Fact] + public async Task The_summary_answers_on_its_route_with_counts_and_zero_filled_buckets() + { + using var admin = await fixture.AdminAsync(); + // An information type of its own, so nothing else the suite created is counted. + var documentId = await Api.CreateAsync(admin, "/api/documents", + new { name = Api.Unique("Summary"), documentFormat = "Json" }); + + var to = new DateTime(2025, 3, 4, 12, 0, 0, DateTimeKind.Utc); + var from = to.AddHours(-1); + string Param(string filter) => "filter=" + Uri.EscapeDataString(filter); + var query = string.Join("&", + Param($"DocumentId:1:{documentId}"), + // Ignored, not refused: the summary is the breakdown the status tabs show. + Param("StatusFilter:1:3"), + Param("ErrorContains:4:connection refused"), + Param("IsRetry:1:false"), + Param($"StartedOn:6:{from:yyyy-MM-ddTHH:mm:ss.fffZ}"), + Param($"StartedOn:8:{to:yyyy-MM-ddTHH:mm:ss.fffZ}")); + + var summary = await Api.Json(await admin.GetAsync($"/api/xchanges/summary?{query}")); + + var counts = summary["counts"]!; + foreach (var status in new[] { "processing", "success", "badResponse", "failed" }) + Assert.Equal(0, (int)counts[status]!); + + // An hour is 61 one-minute buckets counting both ends, one too many, so five minutes. + Assert.Equal(5, (int)summary["bucketMinutes"]!); + var buckets = summary["buckets"]!.AsArray(); + Assert.Equal(13, buckets.Count); + Assert.Equal(from, DateTime.Parse((string)buckets[0]!["start"]!, null, + System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal)); + Assert.All(buckets, b => Assert.Equal(0, (int)b!["failed"]!)); + } +} diff --git a/SW.Bitween.HttpTests/GatewayActivityTests.cs b/SW.Bitween.HttpTests/GatewayActivityTests.cs new file mode 100644 index 00000000..7176c5ff --- /dev/null +++ b/SW.Bitween.HttpTests/GatewayActivityTests.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// What a gateway's partners' calls did: the calls turned away and why, and when each key was last +/// used - the two things "the partner says they get 401" needed and had nowhere to look. +/// +[Collection("Http")] +public class GatewayActivityTests(HttpFixture fixture) +{ + [Fact] + public async Task Refused_calls_are_counted_by_reason_and_a_used_key_says_when() + { + var (admin, urlName, gatewayId, partnerId, _) = await Api.GatewayAsync(fixture); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("primary", key)); + + using (var refused = await Api.CallSyncAsync(fixture, urlName, "not-a-key")) + Assert.Equal(401, (int)refused.StatusCode); + using (var accepted = await Api.CallSyncAsync(fixture, urlName, key)) + Assert.True(accepted.IsSuccessStatusCode, await accepted.Content.ReadAsStringAsync()); + + var rejections = await Api.Json(await admin.GetAsync($"/api/apigateways/{gatewayId}/rejections")); + Assert.Equal(1, (int)rejections["counts"]!["not-authenticated"]!); + var recent = rejections["recent"]!.AsArray().Single()!; + Assert.Equal("not-authenticated", (string)recent["reason"]); + Assert.Equal(401, (int)recent["status"]!); + + var partner = await Api.Json(await admin.GetAsync($"/api/partners/{partnerId}")); + Assert.NotNull(partner["keysLastUsedOn"]!["primary"]); + } +} diff --git a/SW.Bitween.HttpTests/HttpFixture.cs b/SW.Bitween.HttpTests/HttpFixture.cs new file mode 100644 index 00000000..5e43c449 --- /dev/null +++ b/SW.Bitween.HttpTests/HttpFixture.cs @@ -0,0 +1,224 @@ +using System.Linq; +using Microsoft.AspNetCore.TestHost; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SW.Bus; +using SW.Bitween.Services; +using SW.EfCoreExtensions; +using Testcontainers.PostgreSql; +using Testcontainers.RabbitMq; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The whole application, hosted in-process and called over HTTP: routing, model binding, JSON, +/// authentication, the rate limiter and the response headers all run as they do in production - +/// everything the integration suite skips by calling handlers directly. Started exactly as +/// Program.Main starts it: migrations, then the seeded-administrator, system-key, key-hashing and +/// stored-settings steps. +/// +public sealed class HttpFixture : IAsyncLifetime +{ + public const string AdminEmail = "admin@Bitween.systems"; + public const string AdminPassword = "Http-Tests-Admin-2026!"; + public const int SignInPerMinute = 30; + public const int RetryCount = 2; + + readonly PostgreSqlContainer postgres = new PostgreSqlBuilder().WithImage("postgres:16").Build(); + // With the management plugin, as production runs it: Ops pages and queue clean-up go through it. + readonly RabbitMqContainer rabbit = NewRabbit(); + readonly string bucket = $"bitween-http-tests-{Guid.NewGuid():N}"; + + public WebApplicationFactory App { get; private set; } = null!; + Dictionary settings = null!; + + /// + /// Another instance of the app on the same database and broker, as a second node in a cluster + /// is. The caller disposes it. + /// + public WebApplicationFactory SecondNode() + { + var node = new BitweenApp(settings); + using var _ = node.CreateClient(); + return node; + } + + /// A client of with no credentials. + public static HttpClient ClientOf(WebApplicationFactory node) + { + var client = node.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost"), + AllowAutoRedirect = false + }); + client.DefaultRequestHeaders.Add("X-Forwarded-For", $"10.{Random.Shared.Next(255)}.{Random.Shared.Next(255)}.{Random.Shared.Next(1, 255)}"); + return client; + } + + /// The broker the app consumes from, for a test to look at or publish to directly. + public string RabbitConnectionString => rabbit.GetConnectionString(); + + /// The app's own database context, to check what an API call left behind. + public async Task InDbAsync(Func> query) + { + await using var scope = App.Services.CreateAsyncScope(); + return await query(scope.ServiceProvider.GetRequiredService()); + } + + /// The broker's queues, by name. + public async Task QueuesAsync() => + (await RabbitCtlAsync("list_queues", "name")).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + /// Runs rabbitmqctl in the broker's container. + public async Task RabbitCtlAsync(params string[] args) => + (await rabbit.ExecAsync(["rabbitmqctl", "-q", .. args])).Stdout; + + public async Task InitializeAsync() + { + await Task.WhenAll(postgres.StartAsync(), rabbit.StartAsync()); + + settings = Settings("PgSql", postgres.GetConnectionString(), rabbit, bucket); + + App = await StartAsync(settings); + } + + /// The settings an install is started with, on the given metadata database and broker. + public static Dictionary Settings(string databaseType, string connectionString, + RabbitMqContainer rabbit, string bucket) => new() + { + ["Bitween:DatabaseType"] = databaseType, + ["ConnectionStrings:BitweenDb"] = connectionString, + ["ConnectionStrings:RabbitMQ"] = rabbit.GetConnectionString(), + ["Bitween:RabbitMqManagementUrl"] = $"http://{rabbit.Hostname}:{rabbit.GetMappedPublicPort(15672)}", + ["Bitween:RabbitMqManagementUsername"] = RabbitMqBuilder.DefaultUsername, + ["Bitween:RabbitMqManagementPassword"] = RabbitMqBuilder.DefaultPassword, + ["Bitween:StorageProvider"] = "Local", + ["CloudFiles:BucketName"] = bucket, + ["Token:Key"] = "http-tests-signing-key-0123456789abcdefghijklmnop", + ["Token:Issuer"] = "http-tests", + ["Token:Audience"] = "http-tests", + ["Bitween:InitialAdminPassword"] = AdminPassword, + ["Bitween:RateLimits:SignInPerMinute"] = SignInPerMinute.ToString(), + ["Bitween:RateLimits:RequestsPerMinute"] = "100000", + }; + + /// A management-enabled broker, as production runs. + public static RabbitMqContainer NewRabbit() => new RabbitMqBuilder() + .WithImage("rabbitmq:3.13-management") + .WithPortBinding(15672, true) + .Build(); + + /// + /// Starts an install as Program.Main does, and clears the seeded administrator's must-change + /// flag, which every install asks for at first sign-in, so a test's session can do something. + /// + public static async Task> StartAsync(IDictionary settings) + { + var app = new BitweenApp(settings); + // Creating a client starts the host. + using (app.CreateClient()) { } + + using var client = ClientOf(app); + var token = await SignInAsync(client, AdminEmail, AdminPassword); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var changed = await client.PostAsJsonAsync("/api/accounts/changePassword", + new { OldPassword = AdminPassword, NewPassword = AdminPassword }); + changed.EnsureSuccessStatusCode(); + return app; + } + + /// A client of signed in as the administrator. + public static async Task AdminOf(WebApplicationFactory node) + { + var client = ClientOf(node); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", await SignInAsync(client, AdminEmail, AdminPassword)); + return client; + } + + public async Task DisposeAsync() + { + if (App != null) await App.DisposeAsync(); + await Task.WhenAll(postgres.DisposeAsync().AsTask(), rabbit.DisposeAsync().AsTask()); + try { Directory.Delete(Path.Combine(Path.GetTempPath(), "SW.CloudFiles.LocalTests", bucket), true); } catch { } + } + + /// A client with no credentials, from its own address so rate limits don't leak between tests. + public HttpClient Client(string forwardedFor = null!) + { + var client = App.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost"), + AllowAutoRedirect = false + }); + client.DefaultRequestHeaders.Add("X-Forwarded-For", forwardedFor ?? $"10.{Random.Shared.Next(255)}.{Random.Shared.Next(255)}.{Random.Shared.Next(1, 255)}"); + return client; + } + + public static async Task SignInAsync(HttpClient client, string email, string password) + { + var response = await client.PostAsJsonAsync("/api/accounts/login", new { Username = email, Password = password }); + response.EnsureSuccessStatusCode(); + using var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + return body.RootElement.GetProperty("jwt").GetString()!; + } + + /// A client signed in as the administrator. + public async Task AdminAsync() + { + var client = Client(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", await SignInAsync(client, AdminEmail, AdminPassword)); + return client; + } + + internal sealed class BitweenApp(IDictionary settings) : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Development"); + foreach (var (key, value) in settings) builder.UseSetting(key, value); + // Each instance its own socket for its resident adapter host. The default is named after + // the process, which is one per node in production; here the nodes share a process, and a + // second node starting took the first one's socket, so its adapters could no longer attach. + builder.ConfigureTestServices(services => + { + var resident = services.LastOrDefault(d => d.ServiceType == typeof(SW.Serverless.Resident.ResidentOptions)) + ?.ImplementationInstance as SW.Serverless.Resident.ResidentOptions; + if (resident == null) return; + var tag = Guid.NewGuid().ToString("N")[..12]; + resident.SocketPath = $"/tmp/bitween-http-{tag}.sock"; + resident.PipeName = $"bitween-http-{tag}"; + }); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var host = builder.Build(); + // As Program.Main does before Run. + host.MigrateDatabase(); + host.SecureSeededAdministrator().SecureSystemPartnerKey().HashStoredPartnerKeys().ApplyStoredSettings(); + // Production retries a failed message five times a minute apart. Twice, a second apart, is + // the same path through the retry queue to the bad one, in a time a test can wait for. + var bus = host.Services.GetRequiredService(); + bus.DefaultRetryCount = RetryCount; + bus.DefaultRetryAfter = 1; + host.Start(); + return host; + } + } +} + +[CollectionDefinition("Http")] +public class HttpCollection : ICollectionFixture; diff --git a/SW.Bitween.HttpTests/MetadataDatabaseTests.cs b/SW.Bitween.HttpTests/MetadataDatabaseTests.cs new file mode 100644 index 00000000..27b3dadf --- /dev/null +++ b/SW.Bitween.HttpTests/MetadataDatabaseTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using DotNet.Testcontainers.Containers; +using Testcontainers.MsSql; +using Testcontainers.MySql; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Bitween installed on each metadata database it supports besides PostgreSQL, which every other +/// test runs on: migrated from nothing, started as Program.Main starts it, then configured and +/// used over HTTP - an information type, a partner with a key, a gateway, a call through the queue +/// answered with its result, and the pages that list them. These databases were only ever checked +/// for model drift before. +/// +[Collection("Http")] +public class MetadataDatabaseTests +{ + [Fact] + public Task A_fresh_install_on_MySql_migrates_starts_and_carries_a_gateway_call_end_to_end() => + RunAsync("MySql", new MySqlBuilder().WithImage("mysql:8.0").WithDatabase("bitween").Build(), + mustStart: true); + + /// Skipped where SQL Server's image can't run - it is x64 only, and an Apple-silicon Docker can't start it. + [SkippableFact] + public Task A_fresh_install_on_MsSql_migrates_starts_and_carries_a_gateway_call_end_to_end() => + RunAsync("MsSql", new MsSqlBuilder().Build(), mustStart: false); + + static async Task RunAsync(string databaseType, DockerContainer database, bool mustStart) + { + var rabbit = HttpFixture.NewRabbit(); + try + { + try + { + await database.StartAsync(); + } + catch (Exception ex) when (!mustStart) + { + Skip.If(true, $"{databaseType} could not run here: {ex.Message}"); + } + await rabbit.StartAsync(); + + var connectionString = databaseType == "MsSql" + ? ((IDatabaseContainer)database).GetConnectionString().Replace("Database=master", "Database=bitween") + : ((IDatabaseContainer)database).GetConnectionString(); + await using var app = await HttpFixture.StartAsync(HttpFixture.Settings(databaseType, connectionString, rabbit, + $"bitween-{databaseType.ToLowerInvariant()}-{Guid.NewGuid():N}")); + + using var admin = await HttpFixture.AdminOf(app); + var (_, urlName, _, partnerId, subscriptionId) = await Api.GatewayAsync(admin); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + + var call = await Api.CallSyncAsync(HttpFixture.ClientOf(app), urlName, key); + Assert.Equal(HttpStatusCode.OK, call.StatusCode); + + foreach (var page in new[] { "/api/documents", "/api/partners", "/api/subscriptions", "/api/apigateways", + "/api/accounts", "/api/roles", $"/api/audit?entityName=Partner&entityKey={partnerId}" }) + await Api.Json(await admin.GetAsync(page)); + Assert.Contains(subscriptionId.ToString(), + (await Api.Json(await admin.GetAsync("/api/subscriptions"))).ToJsonString()); + + // The metrics are written and read with grouped queries each database translates its own + // way: write this node's counts, then ask every metrics page. + await MetricsHttpTests.FlushAsync(app); + foreach (var page in MetricsHttpTests.Pages(subscriptionId)) + await Api.Json(await admin.GetAsync(page)); + var overview = await Api.Json(await admin.GetAsync("/api/metrics/overview")); + Assert.True((int)overview["last24h"]!["total"]! >= 1, overview.ToJsonString()); + } + finally + { + await Task.WhenAll(database.DisposeAsync().AsTask(), rabbit.DisposeAsync().AsTask()); + } + } +} diff --git a/SW.Bitween.HttpTests/MetricsHttpTests.cs b/SW.Bitween.HttpTests/MetricsHttpTests.cs new file mode 100644 index 00000000..caf096a3 --- /dev/null +++ b/SW.Bitween.HttpTests/MetricsHttpTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Net; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Services.Metrics; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The metrics endpoints over HTTP, after a real gateway call: the call is counted for its +/// subscription and partner, and every page of metrics answers. +/// +[Collection("Http")] +public class MetricsHttpTests(HttpFixture fixture) +{ + /// Every metrics endpoint, for checking each answers on each database. + public static string[] Pages(int subscriptionId) => + [ + "/api/metrics/overview", "/api/metrics/adapters?days=7", "/api/metrics/adapter?id=nativehttphandler", + "/api/metrics/subscriptions?days=7", $"/api/metrics/subscription?id={subscriptionId}&days=7", + "/api/metrics/partners?days=7", "/api/metrics/nodes", "/api/metrics/insights", + ]; + + /// Writes what the node has counted so far, rather than wait the minute the flusher does. + public static Task FlushAsync(WebApplicationFactory app) => + app.Services.GetRequiredService().FlushAsync(CancellationToken.None); + + [Fact] + public async Task A_gateway_call_is_counted_and_every_metrics_page_answers() + { + var (admin, urlName, _, partnerId, subscriptionId) = await Api.GatewayAsync(fixture); + using var _ = admin; + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + Assert.Equal(HttpStatusCode.OK, (await Api.CallSyncAsync(fixture, urlName, key)).StatusCode); + + await FlushAsync(fixture.App); + foreach (var page in Pages(subscriptionId)) await Api.Json(await admin.GetAsync(page)); + + var subscription = await Api.Json(await admin.GetAsync($"/api/metrics/subscription?id={subscriptionId}&days=1")); + Assert.Equal(1, (int)subscription["total"]!["total"]!); + Assert.Equal(1, (int)subscription["total"]!["succeeded"]!); + Assert.NotNull(subscription["total"]!["p95Ms"]); + + var partners = await Api.Json(await admin.GetAsync("/api/metrics/partners?days=1")); + var partner = partners["partners"]!.AsArray().Single(p => (int)p!["partnerId"]! == partnerId)!; + Assert.Equal(1, (int)partner["exchanges"]!); + Assert.NotNull(partner["keyLastUsedOn"]); + } + + [Fact] + public async Task A_refused_gateway_call_is_kept_in_the_week_across_nodes_and_restarts() + { + var (admin, urlName, gatewayId, _, _) = await Api.GatewayAsync(fixture); + using var _ = admin; + Assert.Equal(HttpStatusCode.Unauthorized, (await Api.CallSyncAsync(fixture, urlName, "not-a-real-key")).StatusCode); + + await FlushAsync(fixture.App); + var rejections = await Api.Json(await admin.GetAsync($"/api/apigateways/{gatewayId}/rejections")); + Assert.Equal(1, (int)rejections["lastSevenDays"]!["not-authenticated"]!); + } + + [Fact] + public async Task A_partner_member_without_dashboard_access_is_refused_the_overview() + { + using var anonymous = fixture.Client(); + Assert.Equal(HttpStatusCode.Unauthorized, (await anonymous.GetAsync("/api/metrics/overview")).StatusCode); + } +} diff --git a/SW.Bitween.HttpTests/OperatorActionTests.cs b/SW.Bitween.HttpTests/OperatorActionTests.cs new file mode 100644 index 00000000..b945e11c --- /dev/null +++ b/SW.Bitween.HttpTests/OperatorActionTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// What members do beyond configuration, in the audit trail: a sign-in sets the member's last sign-in, +/// and a retry says who retried what. +/// +[Collection("Http")] +public class OperatorActionTests(HttpFixture fixture) +{ + [Fact] + public async Task A_sign_in_is_kept_on_the_member_and_a_retry_is_in_the_trail() + { + var before = DateTime.UtcNow.AddSeconds(-5); + var (admin, urlName, _, partnerId, subscriptionId) = await Api.GatewayAsync(fixture); + + var members = await Api.Json(await admin.GetAsync("/api/accounts?limit=500")); + var me = members["result"]!.AsArray().Single(m => (string)m!["email"] == HttpFixture.AdminEmail)!; + Assert.True(DateTime.Parse((string)me["lastSignInOn"]!).ToUniversalTime() >= before); + + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("primary", key)); + using (var call = await Api.CallSyncAsync(fixture, urlName, key)) { } + var exchangeId = await fixture.InDbAsync(db => db.Set().AsNoTracking() + .Where(x => x.SubscriptionId == subscriptionId).Select(x => x.Id).FirstAsync()); + + var retried = await admin.PostAsJsonAsync($"/api/xchanges/{exchangeId}/retry", new { reason = "test", reset = false }); + Assert.True(retried.IsSuccessStatusCode, await retried.Content.ReadAsStringAsync()); + + var trail = await Api.Json(await admin.GetAsync("/api/audit?entityName=OperatorAction&limit=50")); + Assert.Contains(trail["result"]!.AsArray(), r => + (string?)r!["changes"]?["Action"]?["new"] == "retry" && (string?)r["changes"]?["Target"]?["new"] == exchangeId); + } +} diff --git a/SW.Bitween.HttpTests/PartnerAndSessionTests.cs b/SW.Bitween.HttpTests/PartnerAndSessionTests.cs new file mode 100644 index 00000000..c83c5124 --- /dev/null +++ b/SW.Bitween.HttpTests/PartnerAndSessionTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Partner keys and sessions over real HTTP, as the admin UI and a partner see them: a key issued and +/// saved on a partner lets that partner call its gateway; revoking the key, or detaching the partner, +/// stops it at once. Signing out ends the session for good. +/// +[Collection("Http")] +public class PartnerAndSessionTests(HttpFixture fixture) +{ + Task<(HttpClient Admin, string UrlName, int GatewayId, int PartnerId, int SubscriptionId)> GatewayAsync() => + Api.GatewayAsync(fixture); + + static Task Json(HttpResponseMessage response) => Api.Json(response); + static Task SetKeysAsync(HttpClient admin, int partnerId, params (string Name, string Value)[] keys) => + Api.SetKeysAsync(admin, partnerId, keys); + static Task NewKeyAsync(HttpClient admin) => Api.NewKeyAsync(admin); + + async Task CallGatewayAsync(string urlName, string key) + { + using var partner = fixture.Client(); + using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/gateway/{urlName}/async") + { + Content = new StringContent("{\"order\":1}", Encoding.UTF8, "application/json") + }; + request.Headers.Add("partnerkey", key); + return (await partner.SendAsync(request)).StatusCode; + } + + [Fact] + public async Task An_issued_key_lets_the_partner_call_its_gateway_and_revoking_it_stops_it() + { + var (admin, urlName, _, partnerId, _) = await GatewayAsync(); + + var key = await NewKeyAsync(admin); + Assert.Matches("^[0-9a-f]{32}$", key); + Assert.NotEqual(key, await NewKeyAsync(admin)); + + Assert.Equal(HttpStatusCode.Unauthorized, await CallGatewayAsync(urlName, key)); + + await SetKeysAsync(admin, partnerId, ("orders-prod", key)); + Assert.Equal(HttpStatusCode.Accepted, await CallGatewayAsync(urlName, key)); + + // The key is never handed back once saved. + var stored = (await Json(await admin.GetAsync($"/api/partners/{partnerId}")))["apiCredentials"]!.AsArray(); + Assert.DoesNotContain(stored, c => (string?)c!["value"] == key); + + await SetKeysAsync(admin, partnerId); + Assert.Equal(HttpStatusCode.Unauthorized, await CallGatewayAsync(urlName, key)); + admin.Dispose(); + } + + [Fact] + public async Task Detaching_a_partner_from_a_gateway_stops_its_calls() + { + var (admin, urlName, gatewayId, partnerId, _) = await GatewayAsync(); + var key = await NewKeyAsync(admin); + await SetKeysAsync(admin, partnerId, ("main", key)); + Assert.Equal(HttpStatusCode.Accepted, await CallGatewayAsync(urlName, key)); + + await Json(await admin.PostAsJsonAsync($"/api/apigateways/{gatewayId}/removepartner", new { partnerId })); + + Assert.Equal(HttpStatusCode.Unauthorized, await CallGatewayAsync(urlName, key)); + admin.Dispose(); + } + + [Fact] + public async Task A_gateway_call_with_no_key_or_a_wrong_one_is_refused() + { + var (admin, urlName, _, partnerId, _) = await GatewayAsync(); + await SetKeysAsync(admin, partnerId, ("main", Guid.NewGuid().ToString("N"))); + + Assert.Equal(HttpStatusCode.Unauthorized, await CallGatewayAsync(urlName, Guid.NewGuid().ToString("N"))); + using var anonymous = fixture.Client(); + Assert.Equal(HttpStatusCode.Unauthorized, + (await anonymous.PostAsync($"/api/gateway/{urlName}/async", new StringContent("{}", Encoding.UTF8, "application/json"))).StatusCode); + admin.Dispose(); + } + + /// The refresh token a response set, from its HttpOnly cookie - never the body. + static string RefreshCookie(HttpResponseMessage response) => + response.Headers.TryGetValues("Set-Cookie", out var cookies) + ? cookies.Select(c => c.Split(';')[0]).Where(c => c.StartsWith("refresh_token=")) + .Select(c => c["refresh_token=".Length..]).LastOrDefault(v => v.Length > 0)! + : null!; + + async Task RefreshAsync(string token) + { + // A fresh client each time, carrying only this cookie, as a browser tab would. + using var client = fixture.Client(); + using var request = new HttpRequestMessage(HttpMethod.Post, "/api/accounts/login") + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }; + request.Headers.Add("Cookie", $"refresh_token={token}"); + return await client.SendAsync(request); + } + + [Fact] + public async Task Signing_out_ends_the_session_and_its_refresh_token() + { + using var client = fixture.Client(); + var login = await client.PostAsJsonAsync("/api/accounts/login", + new { Username = HttpFixture.AdminEmail, Password = HttpFixture.AdminPassword }); + Assert.True(login.IsSuccessStatusCode); + var token = RefreshCookie(login); + Assert.False(string.IsNullOrEmpty(token), "sign-in sets the refresh token as a cookie"); + Assert.DoesNotContain(token, await login.Content.ReadAsStringAsync()); + + // The cookie opens a new session while signed in, and hands over the next token. + var refreshed = await RefreshAsync(token); + Assert.True(refreshed.IsSuccessStatusCode); + var current = RefreshCookie(refreshed); + Assert.False(string.IsNullOrEmpty(current)); + Assert.NotEqual(token, current); + // Superseded, but within its 30-second grace - another tab refreshing at the same moment + // still gets through. + Assert.True((await RefreshAsync(token)).IsSuccessStatusCode, "a just-superseded token is honoured in its grace"); + + // Signed out from a client holding only the current token - as the browser does: its cookie + // jar has just the one, the latest the server set. + using var browser = fixture.Client(); + using (var logout = new HttpRequestMessage(HttpMethod.Post, "/api/accounts/logout") + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }) + { + logout.Headers.Add("Cookie", $"refresh_token={current}"); + var response = await browser.SendAsync(logout); + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"cookies\"", string.Join(",", response.Headers.GetValues("Clear-Site-Data"))); + } + + var after = await RefreshAsync(current); + Assert.False(after.IsSuccessStatusCode, + $"a signed-out refresh token must not open a new session: {(int)after.StatusCode} {await after.Content.ReadAsStringAsync()}"); + } +} diff --git a/SW.Bitween.HttpTests/PipelineTests.cs b/SW.Bitween.HttpTests/PipelineTests.cs new file mode 100644 index 00000000..71f80bda --- /dev/null +++ b/SW.Bitween.HttpTests/PipelineTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// What only a request through the real pipeline can show: which routes answer a caller with no +/// token, that a token from sign-in is honoured, that sign-in is rate limited per address, and that +/// the headers a browser relies on are sent. +/// +[Collection("Http")] +public class PipelineTests(HttpFixture fixture) +{ + /// + /// The only handlers callable without a session. Each answers for itself: the client config is + /// public by design, sign-in and sign-out are how a session starts and ends, and the two older + /// partner endpoints authenticate the partner's key in the handler. A new entry here is a new + /// door into the API, so the list is pinned rather than discovered. + /// + static readonly string[] UnprotectedHandlers = + [ + // Redeems a signed one-time code with the CLI's PKCE verifier: no session to have yet. + "SW.Bitween.Resources.Accounts.CliToken", + "SW.Bitween.Resources.Accounts.Login", + "SW.Bitween.Resources.Accounts.Logout", + // Trades a service account's token for a session: the token is the credential. + "SW.Bitween.Resources.Accounts.ServiceTokenSignIn", + "SW.Bitween.Resources.Settings.Config", + "SW.Bitween.Resources.Xchanges.Get", + "SW.Bitween.Resources.Xchanges.Update", + ]; + + static IEnumerable HandlerTypes() => + typeof(BitweenDbContext).Assembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false } && + t.Namespace?.StartsWith("SW.Bitween.Resources.") == true && + t.GetInterfaces().Any(i => i.Namespace == "SW.PrimitiveTypes" && i.Name.Contains("Handler"))); + + [Fact] + public void Only_the_known_handlers_are_open_without_a_session() + { + var open = HandlerTypes() + .Where(t => t.GetCustomAttributes().Any(a => a.GetType().Name == "UnprotectAttribute")) + .Select(t => t.FullName) + .OrderBy(n => n) + .ToList(); + + Assert.Equal(UnprotectedHandlers.OrderBy(n => n), open); + } + + public static IEnumerable Resources() => + HandlerTypes().Select(t => t.Namespace!.Split('.').Last().ToLowerInvariant()) + .Distinct().OrderBy(r => r).Select(r => new object[] { r }); + + /// + /// Every resource refuses a caller with no token - its list, a record by key, a command - rather + /// than answering, which is what a route that slipped out of protection would do. + /// + [Theory] + [MemberData(nameof(Resources))] + public async Task Every_resource_refuses_a_caller_without_a_token(string resource) + { + using var client = fixture.Client(); + + var calls = new List<(string Method, string Path, HttpResponseMessage Response)> + { + ("GET", $"/api/{resource}", await client.GetAsync($"/api/{resource}")), + ("POST", $"/api/{resource}", await client.PostAsync($"/api/{resource}", Json())) + }; + // A record by key, except the one partner endpoint that reads a key and authenticates the + // partner itself (Xchanges.Get, pinned above). + if (resource != "xchanges") + calls.Add(("GET", $"/api/{resource}/1", await client.GetAsync($"/api/{resource}/1"))); + + foreach (var (method, path, response) in calls) + Assert.False(response.IsSuccessStatusCode, + $"{method} {path} answered {(int)response.StatusCode} to a caller with no token"); + } + + public static IEnumerable NamedHandlers() => + HandlerTypes() + .Where(t => t.GetCustomAttributes().All(a => a.GetType().Name != "UnprotectAttribute")) + .Select(t => (Type: t, Name: (string)t.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name == "HandlerNameAttribute")?.GetType() + .GetProperty("Name")?.GetValue(t.GetCustomAttributes().First(a => a.GetType().Name == "HandlerNameAttribute"))!)) + .Where(h => !string.IsNullOrEmpty(h.Name)) + .Select(h => new object[] { h.Type.Namespace!.Split('.').Last().ToLowerInvariant(), h.Name.ToLowerInvariant(), h.Type.Name }) + .OrderBy(o => o[0]).ThenBy(o => o[1]); + + /// + /// Every named operation - "requeuedeadletters", "generatekey", "setroles", … - answers a caller + /// with no token with 401 on whichever of its routes exists. A named handler is easy to add and + /// easy to leave unguarded, and it is not reached by the resource-level calls above. + /// + [Theory] + [MemberData(nameof(NamedHandlers))] + public async Task Every_named_operation_refuses_a_caller_without_a_token(string resource, string name, string handler) + { + using var client = fixture.Client(); + var paths = new[] { $"/api/{resource}/{name}", $"/api/{resource}/1/{name}" }; + + var responses = new List<(string Call, HttpStatusCode Status)>(); + foreach (var path in paths) + { + responses.Add(($"GET {path}", (await client.GetAsync(path)).StatusCode)); + responses.Add(($"POST {path}", (await client.PostAsync(path, Json())).StatusCode)); + } + + Assert.DoesNotContain(responses, r => (int)r.Status is >= 200 and < 300); + Assert.True(responses.Any(r => r.Status == HttpStatusCode.Unauthorized), + $"{handler}: no route answered 401 - {string.Join(", ", responses.Select(r => $"{r.Call} {(int)r.Status}"))}"); + } + + static StringContent Json() => new("{}", Encoding.UTF8, "application/json"); + + [Fact] + public async Task A_token_from_sign_in_is_honoured_and_a_forged_one_is_not() + { + using var admin = await fixture.AdminAsync(); + var ok = await admin.GetAsync("/api/subscriptions"); + Assert.Equal(HttpStatusCode.OK, ok.StatusCode); + + using var forged = fixture.Client(); + var token = admin.DefaultRequestHeaders.Authorization!.Parameter!; + var parts = token.Split('.'); + // Same header and claims, signature of another key. + forged.DefaultRequestHeaders.Authorization = new("Bearer", $"{parts[0]}.{parts[1]}.{Convert.ToBase64String(new byte[32]).TrimEnd('=').Replace('+', '-').Replace('/', '_')}"); + Assert.Equal(HttpStatusCode.Unauthorized, (await forged.GetAsync("/api/subscriptions")).StatusCode); + } + + [Fact] + public async Task The_public_config_answers_without_a_token() + { + using var client = fixture.Client(); + var response = await client.GetAsync("/api/settings/config"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Sign_in_is_rate_limited_per_address() + { + using var client = fixture.Client(forwardedFor: "203.0.113.77"); + var statuses = new List(); + for (var i = 0; i < HttpFixture.SignInPerMinute + 2; i++) + statuses.Add((await client.PostAsJsonAsync("/api/accounts/login", + new { Username = "nobody@example.test", Password = "wrong" })).StatusCode); + + Assert.DoesNotContain(HttpStatusCode.TooManyRequests, statuses.Take(HttpFixture.SignInPerMinute)); + Assert.Equal(HttpStatusCode.TooManyRequests, statuses.Last()); + + // Another address keeps its own budget. + using var other = fixture.Client(forwardedFor: "203.0.113.78"); + Assert.NotEqual(HttpStatusCode.TooManyRequests, + (await other.PostAsJsonAsync("/api/accounts/login", new { Username = "nobody@example.test", Password = "wrong" })).StatusCode); + } + + [Fact] + public async Task Responses_carry_the_security_headers() + { + using var client = fixture.Client(); + var response = await client.GetAsync("/api/settings/config"); + + Assert.Equal("DENY", Header(response, "X-Frame-Options")); + Assert.Equal("nosniff", Header(response, "X-Content-Type-Options")); + Assert.Equal("strict-origin-when-cross-origin", Header(response, "Referrer-Policy")); + } + + [Fact] + public async Task Health_endpoints_answer_without_a_token() + { + using var client = fixture.Client(); + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/health/live")).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/health/ready")).StatusCode); + } + + static string Header(HttpResponseMessage response, string name) => + response.Headers.TryGetValues(name, out var values) ? values.First() + : response.Content.Headers.TryGetValues(name, out var content) ? content.First() : null!; +} diff --git a/SW.Bitween.HttpTests/QueueTests.cs b/SW.Bitween.HttpTests/QueueTests.cs new file mode 100644 index 00000000..d08065c2 --- /dev/null +++ b/SW.Bitween.HttpTests/QueueTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Messages through the real broker to the real consumers. The app here starts its bus consumers as +/// production does, so nothing reaches a consumer except through RabbitMQ. +/// +[Collection("Http")] +public class QueueTests(HttpFixture fixture) +{ + /// + /// The gateway only queues the exchange and then waits for its result, so an answer of 200 means + /// the consumer took the message off the queue and processed it. + /// + [Fact] + public async Task A_gateway_call_is_consumed_from_the_queue_and_answered_with_its_result() + { + var (admin, urlName, _, partnerId, _) = await Api.GatewayAsync(fixture); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + admin.Dispose(); + + using var partner = fixture.Client(); + using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/gateway/{urlName}/sync") + { + Content = new StringContent("{\"order\":42}", Encoding.UTF8, "application/json") + }; + request.Headers.Add("partnerkey", key); + request.Headers.Add("Wait-Period", "30"); + var response = await partner.SendAsync(request); + + var body = await response.Content.ReadAsStringAsync(); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"{(int)response.StatusCode}: {body}"); + Assert.Matches("^\"?[0-9a-f]{32}\"?$", body); + } + + /// + /// A message the consumer cannot read is retried through the retry queue and then parked in the + /// bad queue - not lost, and not retried for ever. + /// + [Fact(Skip = "SimplyWorks.Bus 8.1.0 to 10.0.1 drop such a message instead of parking it: the ack comes " + + "before the bad-queue publish, which always throws. Fixed in SW-Bus fix/dead-letter-lost; " + + "unskip once Bitween is on the release carrying it.")] + public async Task A_message_the_consumer_cannot_process_is_retried_and_then_dead_lettered() + { + var lane = WorkGroup.None.GetBusMessageName(); + var bad = $"v3.development.bitween.xchangeservice.{lane.ToLowerInvariant()}.bad"; + var before = await DepthAsync(bad); + + await using (var scope = fixture.App.Services.CreateAsyncScope()) + await scope.ServiceProvider.GetRequiredService().Publish(lane, "this is not an exchange message"); + + var clock = Stopwatch.StartNew(); + var depth = before; + while (depth == before && clock.Elapsed < TimeSpan.FromSeconds(30)) + { + await Task.Delay(500); + depth = await DepthAsync(bad); + } + + Assert.Equal(before + 1, depth); + // Retried before it was parked: RetryCount retries a second apart. + Assert.True(clock.Elapsed >= TimeSpan.FromSeconds(HttpFixture.RetryCount - 0.5), + $"dead-lettered after {clock.ElapsedMilliseconds} ms, too soon to have been retried"); + } + + async Task DepthAsync(string queue) + { + var lines = (await fixture.RabbitCtlAsync("list_queues", "name", "messages")).Split('\n'); + var row = lines.Select(l => l.Split('\t')).FirstOrDefault(c => c.Length == 2 && c[0] == queue); + Assert.True(row != null, $"no queue {queue}: {string.Join(", ", lines)}"); + return int.Parse(row![1].Trim()); + } +} diff --git a/SW.Bitween.HttpTests/ReadTests.cs b/SW.Bitween.HttpTests/ReadTests.cs new file mode 100644 index 00000000..3b0e5ae0 --- /dev/null +++ b/SW.Bitween.HttpTests/ReadTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace SW.Bitween.HttpTests; + +/// +/// Every screen's read, opened by an administrator over HTTP on a running install: lists, and the +/// named views (operations, dashboard, audit, run history). Any of them failing is a page that +/// shows an error instead of its data. +/// +[Collection("Http")] +public class ReadTests(HttpFixture fixture, ITestOutputHelper output) +{ + static IEnumerable Handlers() => + typeof(BitweenDbContext).Assembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false } && + t.Namespace?.StartsWith("SW.Bitween.Resources.") == true); + + static bool Implements(Type t, string name) => t.GetInterfaces().Any(i => i.Namespace == "SW.PrimitiveTypes" && i.Name.StartsWith(name)); + + static string? NameOf(Type t) + { + var attribute = t.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == "HandlerNameAttribute"); + return (string?)attribute?.GetType().GetProperty("Name")?.GetValue(attribute); + } + + static string Resource(Type t) => t.Namespace!.Split('.').Last().ToLowerInvariant(); + + /// The paths an admin's pages read: each resource's list, and each named query with no key. + public static IEnumerable Reads() => + Handlers().Where(t => Implements(t, "ISearchyHandler")).Select(t => $"/api/{Resource(t)}") + .Concat(Handlers() + .Where(t => t.GetInterfaces().Any(i => i.Namespace == "SW.PrimitiveTypes" && i.Name == "IQueryHandler`1") + && NameOf(t) is not null) + .Select(t => $"/api/{Resource(t)}/{NameOf(t)!.ToLowerInvariant()}")) + .Distinct().OrderBy(p => p).Select(p => new object[] { p }); + + [Theory] + [MemberData(nameof(Reads))] + public async Task An_administrator_s_read_answers_without_a_server_error(string path) + { + using var admin = await fixture.AdminAsync(); + var response = await admin.GetAsync(path); + var body = await response.Content.ReadAsStringAsync(); + output.WriteLine($"{(int)response.StatusCode} {path}"); + Assert.True((int)response.StatusCode < 500, $"{(int)response.StatusCode} {path}: {body[..Math.Min(body.Length, 600)]}"); + Assert.True(response.IsSuccessStatusCode, $"{(int)response.StatusCode} {path}: {body[..Math.Min(body.Length, 600)]}"); + } + + /// Named queries that take a request, opened with none: a bad request is a fair answer, a crash is not. + public static IEnumerable ParameterisedReads() => + Handlers() + .Where(t => t.GetInterfaces().Any(i => i.Namespace == "SW.PrimitiveTypes" && i.Name == "IQueryHandler`2") + && NameOf(t) is not null) + .Select(t => $"/api/{Resource(t)}/{NameOf(t)!.ToLowerInvariant()}") + .Distinct().OrderBy(p => p).Select(p => new object[] { p }); + + [Theory] + [MemberData(nameof(ParameterisedReads))] + public async Task A_read_asked_without_its_parameters_is_refused_not_crashed(string path) + { + using var admin = await fixture.AdminAsync(); + var response = await admin.GetAsync(path); + var body = await response.Content.ReadAsStringAsync(); + output.WriteLine($"{(int)response.StatusCode} {path}"); + Assert.True((int)response.StatusCode < 500, $"{(int)response.StatusCode} {path}: {body[..Math.Min(body.Length, 600)]}"); + } + + /// Every command and delete that names a record, by the route the API gives it. + public static IEnumerable KeyedCommands() => + Handlers() + .SelectMany(t => t.GetInterfaces() + .Where(i => i.Namespace == "SW.PrimitiveTypes" && i.Name is "ICommandHandler`3" or "IDeleteHandler`1") + .Select(i => (Method: i.Name == "IDeleteHandler`1" ? "DELETE" : "POST", + Path: NameOf(t) is { } name ? $"/api/{Resource(t)}/999999/{name.ToLowerInvariant()}" : $"/api/{Resource(t)}/999999"))) + .Distinct().OrderBy(c => c.Path).ThenBy(c => c.Method) + .Select(c => new object[] { c.Method, c.Path }); + + /// + /// A record that isn't there - deleted in another tab, a stale link - is the caller's mistake to + /// be told about, not a server error. + /// + [Theory] + [MemberData(nameof(KeyedCommands))] + public async Task A_command_on_a_record_that_does_not_exist_is_refused_not_crashed(string method, string path) + { + using var admin = await fixture.AdminAsync(); + using var request = new System.Net.Http.HttpRequestMessage(new System.Net.Http.HttpMethod(method), path); + if (method == "POST") request.Content = System.Net.Http.Json.JsonContent.Create(new { }); + var response = await admin.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + output.WriteLine($"{(int)response.StatusCode} {method} {path}"); + Assert.True((int)response.StatusCode < 500, $"{(int)response.StatusCode} {method} {path}: {body[..Math.Min(body.Length, 400)]}"); + } + + /// Every read of one record, by a key that isn't there. + public static IEnumerable KeyedReads() => + Handlers().Where(t => t.GetInterfaces().Any(i => i.Namespace == "SW.PrimitiveTypes" && i.Name == "IGetHandler`2")) + .Select(t => NameOf(t) is { } name ? $"/api/{Resource(t)}/999999/{name.ToLowerInvariant()}" : $"/api/{Resource(t)}/999999") + .Distinct().OrderBy(p => p).Select(p => new object[] { p }); + + /// + /// Reading a record that isn't there - a stale link, one deleted in another tab - is answered, + /// not crashed: a subscription that didn't exist was a NullReferenceException and a 500, which + /// the page could only show as a server error. + /// + [Theory] + [MemberData(nameof(KeyedReads))] + public async Task A_read_of_a_record_that_does_not_exist_is_answered_not_crashed(string path) + { + using var admin = await fixture.AdminAsync(); + var response = await admin.GetAsync(path); + var body = await response.Content.ReadAsStringAsync(); + output.WriteLine($"{(int)response.StatusCode} GET {path}"); + Assert.True((int)response.StatusCode < 500, $"{(int)response.StatusCode} GET {path}: {body[..Math.Min(body.Length, 400)]}"); + } +} diff --git a/SW.Bitween.HttpTests/SW.Bitween.HttpTests.csproj b/SW.Bitween.HttpTests/SW.Bitween.HttpTests.csproj new file mode 100644 index 00000000..4b21af6f --- /dev/null +++ b/SW.Bitween.HttpTests/SW.Bitween.HttpTests.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/SW.Bitween.HttpTests/SchedulerTests.cs b/SW.Bitween.HttpTests/SchedulerTests.cs new file mode 100644 index 00000000..7a6ab40f --- /dev/null +++ b/SW.Bitween.HttpTests/SchedulerTests.cs @@ -0,0 +1,160 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// The scheduler for real: the hosted app runs Quartz as production does, and a receiving +/// subscription pulls from a partner's feed served on a real port. "Receive now" fires the job, +/// and a schedule fires on its own at its minute. +/// +[Collection("Http")] +public class SchedulerTests(HttpFixture fixture) +{ + sealed class Feed : IAsyncDisposable + { + readonly WebApplication app; + int calls; + public int Calls => calls; + public string Url { get; } + + Feed(WebApplication app, string url) => (this.app, Url) = (app, url); + + public static async Task StartAsync() + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + var app = builder.Build(); + Feed feed = null!; + app.MapGet("/shipments", () => + { + Interlocked.Increment(ref feed.calls); + return Results.Json(new object[] { new { id = 1, carrier = "DHL" }, new { id = 2, carrier = "UPS" } }); + }); + await app.StartAsync(); + var url = app.Services.GetRequiredService().Features.Get()!.Addresses.First(); + return feed = new Feed(app, url + "/shipments"); + } + + public ValueTask DisposeAsync() => app.DisposeAsync(); + } + + static async Task EventuallyAsync(Func> condition, TimeSpan within) + { + var clock = Stopwatch.StartNew(); + while (clock.Elapsed < within) + { + if (await condition()) return true; + await Task.Delay(500); + } + return await condition(); + } + + async Task CreateReceiverAsync(System.Net.Http.HttpClient admin, string feedUrl, int minute) + { + var documentId = await Api.CreateAsync(admin, "/api/documents", new { name = Api.Unique("Shipment"), documentFormat = "Json" }); + return await Api.CreateAsync(admin, "/api/subscriptions", new + { + name = Api.Unique("Shipment pull"), + documentId, + type = "Receiving", + inactive = false, + receiverId = "NativeHttpReceiver", + receiverProperties = new[] + { + new { key = "Url", value = feedUrl }, + new { key = "Verb", value = "get" }, + new { key = "ContentType", value = "application/json" }, + }, + schedules = new[] { new { recurrence = "Hourly", days = 0, hours = 0, minutes = minute } }, + }); + } + + Task AttemptsAsync(int subscriptionId) => + fixture.InDbAsync(db => db.Set().AsNoTracking() + .Where(a => a.SubscriptionId == subscriptionId).OrderBy(a => a.StartedOn).ToArrayAsync()); + + /// + /// A run killed mid-way leaves the flag set, and every later run is skipped as already running + /// until the flag goes stale. Clearing it is now an action, refused only while a run is live. + /// + [Fact] + public async Task A_stuck_running_flag_can_be_cleared_and_says_who_did() + { + using var admin = await fixture.AdminAsync(); + await using var feed = await Feed.StartAsync(); + var id = await CreateReceiverAsync(admin, feed.Url, (DateTime.UtcNow.Minute + 30) % 60); + + // As a run that died with its process leaves it. + await fixture.InDbAsync(db => db.Set().Where(s => s.Id == id) + .ExecuteUpdateAsync(u => u.SetProperty(s => s.IsRunning, true).SetProperty(s => s.RunningSince, DateTime.UtcNow))); + var health = await Api.Json(await admin.GetAsync("/api/subscriptions/schedulehealth")); + Assert.True((bool)health.AsArray().Single(h => (int)h!["subscriptionId"]! == id)!["stuck"]!); + + var cleared = await Api.Json(await admin.PostAsJsonAsync($"/api/subscriptions/{id}/clearrunning", new { })); + Assert.True((bool)cleared["cleared"]!); + Assert.False(await fixture.InDbAsync(db => db.Set().Where(s => s.Id == id).Select(s => s.IsRunning).SingleAsync())); + + health = await Api.Json(await admin.GetAsync("/api/subscriptions/schedulehealth")); + Assert.False((bool)health.AsArray().Single(h => (int)h!["subscriptionId"]! == id)!["stuck"]!); + + // Cleared through the context, so the trail has it. + var trail = await Api.Json(await admin.GetAsync($"/api/audit?entityName=Subscription&entityKey={id}&limit=20")); + Assert.Contains(trail["result"]!.AsArray(), r => r!["changes"]?["IsRunning"] != null); + } + + [Fact] + public async Task Receive_now_runs_the_job_which_pulls_the_feed_into_exchanges() + { + await using var feed = await Feed.StartAsync(); + using var admin = await fixture.AdminAsync(); + // A schedule that won't come round during the test. + var subscriptionId = await CreateReceiverAsync(admin, feed.Url, (DateTime.Now.Minute + 30) % 60); + + await Api.Json(await admin.PostAsJsonAsync($"/api/subscriptions/{subscriptionId}/receivenow", new { })); + + Assert.True(await EventuallyAsync(async () => (await AttemptsAsync(subscriptionId)).Length == 1, TimeSpan.FromSeconds(60)), + "receive now never ran the job"); + var attempt = (await AttemptsAsync(subscriptionId)).Single(); + Assert.Equal(ReceiveOutcome.Received, attempt.Outcome); + Assert.Equal(2, attempt.ExchangeIds.Length); + Assert.Equal(1, feed.Calls); + Assert.Equal(2, await fixture.InDbAsync(db => db.Set().CountAsync(x => x.SubscriptionId == subscriptionId))); + + Assert.Equal(HttpStatusCode.NotFound, + (await admin.PostAsJsonAsync("/api/subscriptions/999999/receivenow", new { })).StatusCode); + } + + [Fact] + public async Task A_schedule_fires_on_its_own_at_its_minute() + { + await using var feed = await Feed.StartAsync(); + using var admin = await fixture.AdminAsync(); + // Far enough into the next minute that saving can't miss it. + var at = DateTime.Now.AddSeconds(DateTime.Now.Second > 45 ? 120 : 60); + var subscriptionId = await CreateReceiverAsync(admin, feed.Url, at.Minute); + + Assert.Equal(0, feed.Calls); + Assert.True(await EventuallyAsync(async () => (await AttemptsAsync(subscriptionId)).Length > 0, TimeSpan.FromSeconds(150)), + $"the {at:HH:mm} schedule never fired"); + Assert.Equal(ReceiveOutcome.Received, (await AttemptsAsync(subscriptionId))[0].Outcome); + Assert.True(feed.Calls >= 1); + } +} diff --git a/SW.Bitween.HttpTests/ServiceAccountTests.cs b/SW.Bitween.HttpTests/ServiceAccountTests.cs new file mode 100644 index 00000000..c64767be --- /dev/null +++ b/SW.Bitween.HttpTests/ServiceAccountTests.cs @@ -0,0 +1,184 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Accounts; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Service accounts over HTTP: a token is shown once, traded for a session that can do what the +/// account's roles allow and no more, and stops working when it's revoked, expires, or its account +/// is disabled or removed. Service accounts are kept apart from members in both directions. +/// +[Collection("Http")] +public class ServiceAccountTests(HttpFixture fixture) +{ + static async Task RefusedAsync(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + Assert.True(!response.IsSuccessStatusCode, $"expected a refusal, got {(int)response.StatusCode}: {body}"); + return body; + } + + Task TradeAsync(string token) => + fixture.Client().PostAsJsonAsync("/api/accounts/servicetoken", new { token }); + + async Task SessionAsync(string token) + { + var answer = await Api.Json(await TradeAsync(token)); + var client = fixture.Client(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string)answer["jwt"]!); + return client; + } + + static async Task<(int Id, string Token, int TokenId)> ServiceAccountAsync(HttpClient admin, params int[] roleIds) + { + var id = await Api.CreateAsync(admin, "/api/serviceaccounts", new { name = Api.Unique("service account"), roleIds }); + var issued = await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/issueToken", + new { name = "first", expiresInDays = 30 })); + return (id, (string)issued["token"]!, (int)issued["id"]!); + } + + static async Task RoleAsync(HttpClient admin, params string[] permissions) => + await Api.CreateAsync(admin, "/api/roles", new { name = Api.Unique("service role"), permissions }); + + static async Task ListedAsync(HttpClient admin, int id) => + (await Api.Json(await admin.GetAsync("/api/serviceaccounts"))).AsArray().Single(a => (int)a!["id"]! == id)!; + + [Fact] + public async Task A_token_is_shown_once_and_its_session_can_do_what_the_roles_allow_and_no_more() + { + using var admin = await fixture.AdminAsync(); + var (id, token, _) = await ServiceAccountAsync(admin, await RoleAsync(admin, "documents.view")); + + Assert.StartsWith("bwt_", token); + Assert.Equal(44, token.Length); + // Never again: not in the list, and not in the account's history either. + var listed = await ListedAsync(admin, id); + Assert.DoesNotContain(token, listed.ToJsonString()); + Assert.Equal(token[..12], (string)listed["tokens"]![0]!["shown"]!); + var stored = await fixture.InDbAsync(db => db.Set().AsNoTracking().SingleAsync(t => t.AccountId == id)); + Assert.NotEqual(token, stored.Hash); + + using var session = await SessionAsync(token); + Assert.True((await session.GetAsync("/api/documents")).IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Forbidden, (await session.PostAsJsonAsync("/api/documents", + new { name = Api.Unique("Invoice"), documentFormat = "Json" })).StatusCode); + Assert.Equal(HttpStatusCode.Forbidden, (await session.GetAsync("/api/partners")).StatusCode); + + // The use is recorded on the token and the account. + listed = await ListedAsync(admin, id); + Assert.NotNull((string?)listed["lastSignInOn"]); + Assert.NotNull((string?)listed["tokens"]![0]!["lastUsedOn"]); + } + + [Fact] + public async Task A_token_stops_working_when_revoked_expired_or_its_account_is_disabled_or_removed() + { + using var admin = await fixture.AdminAsync(); + var (id, first, firstId) = await ServiceAccountAsync(admin, await RoleAsync(admin, "documents.view")); + + // Two at a time, so one can replace the other; a third is refused until one goes. + var second = (string)(await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/issueToken", + new { name = "second" })))["token"]!; + Assert.Contains("TOO_MANY_TOKENS", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/serviceaccounts/{id}/issueToken", new { name = "third" }))); + Assert.Contains("ExpiresInDays", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/serviceaccounts/{id}/issueToken", new { name = "too long", expiresInDays = 400 }))); + + await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/revokeToken", new { tokenId = firstId })); + Assert.Contains("isn't valid", await RefusedAsync(await TradeAsync(first))); + Assert.Equal("revoked", (string)(await ListedAsync(admin, id))["tokens"]!.AsArray() + .Single(t => (int)t!["id"]! == firstId)!["status"]!); + + // Disabling ends sessions already open, not only new ones. + using var session = await SessionAsync(second); + await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/setDisabled", new { disabled = true })); + Assert.Equal(HttpStatusCode.Forbidden, (await session.GetAsync("/api/documents")).StatusCode); + await RefusedAsync(await TradeAsync(second)); + await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/setDisabled", new { disabled = false })); + (await SessionAsync(second)).Dispose(); + + // Expired, by the clock rather than by anyone. + await fixture.InDbAsync(db => db.Set().Where(t => t.AccountId == id && t.RevokedOn == null) + .ExecuteUpdateAsync(u => u.SetProperty(t => t.ExpiresOn, DateTime.UtcNow.AddMinutes(-1)))); + await RefusedAsync(await TradeAsync(second)); + Assert.Equal("expired", (string)(await ListedAsync(admin, id))["tokens"]![0]!["status"]!); + + // Removed: gone from the list, its tokens revoked with it. + var third = (string)(await Api.Json(await admin.PostAsJsonAsync($"/api/serviceaccounts/{id}/issueToken", + new { name = "last" })))["token"]!; + (await SessionAsync(third)).Dispose(); + await Api.Json(await admin.DeleteAsync($"/api/serviceaccounts/{id}")); + await RefusedAsync(await TradeAsync(third)); + Assert.DoesNotContain((await Api.Json(await admin.GetAsync("/api/serviceaccounts"))).AsArray(), a => (int)a!["id"]! == id); + } + + [Fact] + public async Task Service_accounts_are_not_members_and_members_are_not_service_accounts() + { + using var admin = await fixture.AdminAsync(); + var (id, token, _) = await ServiceAccountAsync(admin, await RoleAsync(admin, "documents.view")); + + // Not in the member list, and out of reach of the member endpoints... + var members = (await Api.Json(await admin.GetAsync("/api/accounts?limit=500")))["result"]!.AsArray(); + Assert.DoesNotContain(members, m => (int)m!["id"]! == id); + Assert.Contains("ACCOUNT_NOT_FOUND", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/accounts/{id}/setPassword", new { password = "Some-Password-2026!" }))); + Assert.Contains("ACCOUNT_NOT_FOUND", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/accounts/{id}/setRoles", new { roleIds = new[] { 1 } }))); + // ...but still named where its changes are shown. + var lookup = await Api.Json(await admin.GetAsync("/api/accounts?lookup=true")); + Assert.NotNull(lookup[id.ToString()]); + + // A session from a token can't make itself a password or a browser sign-in. + using var session = await SessionAsync(token); + await RefusedAsync(await session.PostAsJsonAsync("/api/accounts/changepassword", + new { oldPassword = "", newPassword = "Some-Password-2026!" })); + + // And a member's id isn't a service account's. + var memberId = members.Select(m => (int)m!["id"]!).First(); + Assert.Contains("SERVICE_ACCOUNT_NOT_FOUND", await RefusedAsync(await admin.PostAsJsonAsync( + $"/api/serviceaccounts/{memberId}/issueToken", new { name = "sneaky" }))); + } + + [Fact] + public async Task Nobody_makes_a_service_account_stronger_than_themselves() + { + using var admin = await fixture.AdminAsync(); + var roleId = await RoleAsync(admin, "documents.view", "service-accounts.view", "service-accounts.create"); + var email = $"svc-maker-{Guid.NewGuid():N}@example.com"[..40]; + const string password = "Member-Pass-2026!"; + await Api.CreateAsync(admin, "/api/accounts", new { name = "Maker", email, password, roleIds = new[] { roleId } }); + using var maker = fixture.Client(); + maker.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", await HttpFixture.SignInAsync(maker, email, password)); + + Assert.Contains("BEYOND_YOUR_PERMISSIONS", await RefusedAsync(await maker.PostAsJsonAsync("/api/serviceaccounts", + new { name = Api.Unique("service account"), roleIds = new[] { Role.AdministratorId } }))); + + // Nor a token for an account that can do more than they can. + var (strongId, _, _) = await ServiceAccountAsync(admin, Role.AdministratorId); + Assert.Contains("ACCOUNT_OUTRANKS_YOU", await RefusedAsync(await maker.PostAsJsonAsync( + $"/api/serviceaccounts/{strongId}/issueToken", new { name = "mine now" }))); + + // Without the grant at all, there's nothing to see. + var (_, token, _) = await ServiceAccountAsync(admin, await RoleAsync(admin, "documents.view")); + using var session = await SessionAsync(token); + Assert.Equal(HttpStatusCode.Forbidden, (await session.GetAsync("/api/serviceaccounts")).StatusCode); + } + + [Fact] + public async Task Something_that_is_not_a_token_is_refused_the_same_way() + { + Assert.Contains("isn't valid", await RefusedAsync(await TradeAsync("bwt_" + new string('x', 40)))); + Assert.Contains("isn't valid", await RefusedAsync(await TradeAsync("not even close"))); + } +} diff --git a/SW.Bitween.HttpTests/UnattendedQueueTests.cs b/SW.Bitween.HttpTests/UnattendedQueueTests.cs new file mode 100644 index 00000000..51dfb4e7 --- /dev/null +++ b/SW.Bitween.HttpTests/UnattendedQueueTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// Queue health's clean-up against the real broker and its management API: a lane no consumer +/// definition declares is listed with what it holds and can be deleted whole; a queue Bitween still +/// consumes, or one something is reading, never is - whatever name the caller sends. +/// +[Collection("Http")] +public class UnattendedQueueTests(HttpFixture fixture) +{ + const string Prefix = "v3.development.bitween"; + + [Fact] + public async Task Only_a_lane_nothing_reads_is_deleted_and_all_three_of_its_queues_go() + { + var orphan = $"{Prefix}.busservice.orphan{Guid.NewGuid():N}"[..60]; + var held = $"{Prefix}.busservice.held{Guid.NewGuid():N}"[..60]; + var attended = $"{Prefix}.xchangeservice.0ungrouped"; + + using var connection = new ConnectionFactory { Uri = new Uri(fixture.RabbitConnectionString) }.CreateConnection(); + using var channel = connection.CreateModel(); + foreach (var queue in new[] { orphan, $"{orphan}.retry", $"{orphan}.bad", held }) + channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false); + channel.BasicPublish("", orphan, null, Encoding.UTF8.GetBytes("left behind 1")); + channel.BasicPublish("", orphan, null, Encoding.UTF8.GetBytes("left behind 2")); + channel.BasicPublish("", $"{orphan}.bad", null, Encoding.UTF8.GetBytes("dead")); + // Something outside Bitween still reads this one. + using var reader = connection.CreateModel(); + reader.BasicConsume(held, autoAck: false, new EventingBasicConsumer(reader)); + + using var admin = await fixture.AdminAsync(); + + // The management API's figures lag the broker by a few seconds. + JsonObject listed = null!; + var clock = Stopwatch.StartNew(); + while (clock.Elapsed < TimeSpan.FromSeconds(30)) + { + var lanes = (await Api.Json(await admin.GetAsync("/api/ops/unattendedqueues"))).AsArray(); + listed = lanes.FirstOrDefault(l => (string?)l!["queueName"] == orphan)?.AsObject()!; + if (listed != null && (long)listed["messages"]! == 2 && (long)listed["deadMessages"]! == 1) break; + await Task.Delay(1000); + } + Assert.NotNull(listed); + Assert.Equal(2, (long)listed["messages"]!); + Assert.Equal(1, (long)listed["deadMessages"]!); + Assert.Equal(3, (long)listed["queues"]!); + + var result = await Api.Json(await admin.PostAsJsonAsync("/api/ops/deleteunattendedqueues", + new { queueNames = new[] { orphan, attended, held } })); + + Assert.Equal([orphan], result["deleted"]!.AsArray().Select(n => (string)n!).ToArray()); + var skipped = result["skipped"]!.AsArray().ToDictionary(s => (string)s!["queueName"]!, s => (string)s!["reason"]!); + Assert.Contains("isn't an unattended queue", skipped[attended]); + Assert.Contains("still reading", skipped[held]); + + var queues = await fixture.QueuesAsync(); + Assert.DoesNotContain(queues, q => q.StartsWith(orphan)); + Assert.Contains(attended, queues); + Assert.Contains(held, queues); + } +} diff --git a/SW.Bitween.HttpTests/WorkGroupTests.cs b/SW.Bitween.HttpTests/WorkGroupTests.cs new file mode 100644 index 00000000..72a1d4b5 --- /dev/null +++ b/SW.Bitween.HttpTests/WorkGroupTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Xunit; + +namespace SW.Bitween.HttpTests; + +/// +/// A work group is its own pair of lanes on the broker. Over HTTP and against the real broker: its +/// subscriptions are processed through its lane, renaming its bus name moves the lane, and deleting +/// it - refused while a subscription is in it - takes the lanes away. +/// +[Collection("Http")] +public class WorkGroupTests(HttpFixture fixture) +{ + async Task EventuallyAsync(Func condition, int seconds = 30) + { + var clock = Stopwatch.StartNew(); + while (clock.Elapsed < TimeSpan.FromSeconds(seconds)) + { + if (condition(await fixture.QueuesAsync())) return true; + await Task.Delay(500); + } + return condition(await fixture.QueuesAsync()); + } + + static string Lane(int id, string busName) => $"v3.development.bitween.xchangeservice.{id}{busName}".ToLowerInvariant(); + + [Fact] + public async Task A_work_group_processes_through_its_own_lane_which_moves_on_rename_and_goes_on_delete() + { + var (admin, urlName, _, partnerId, subscriptionId) = await Api.GatewayAsync(fixture); + var key = await Api.NewKeyAsync(admin); + await Api.SetKeysAsync(admin, partnerId, ("main", key)); + + var busName = $"Priority{Guid.NewGuid():N}"[..16]; + var groupId = (int)(await Api.Json(await admin.PostAsJsonAsync("/api/workgroups", new + { + name = Api.Unique("Priority"), busMessageName = busName, + options = new { rabbitMqOptions = new { prefetch = 5 } } + })))["id"]!; + var lane = Lane(groupId, busName); + Assert.True(await EventuallyAsync(q => q.Contains(lane) && q.Contains($"{lane}-result")), $"no lanes {lane}"); + + async Task MoveSubscriptionAsync(int? workGroupId) + { + var subscription = (await Api.Json(await admin.GetAsync($"/api/subscriptions/{subscriptionId}"))).AsObject(); + subscription["workGroupId"] = workGroupId; + await Api.Json(await admin.PostAsJsonAsync($"/api/subscriptions/{subscriptionId}", subscription)); + } + + await MoveSubscriptionAsync(groupId); + Assert.Equal(HttpStatusCode.OK, (await Api.CallSyncAsync(fixture, urlName, key)).StatusCode); + + var refused = await admin.PostAsJsonAsync($"/api/workgroups/{groupId}/delete", new { }); + Assert.False(refused.IsSuccessStatusCode); + Assert.Contains("CANT_BE_DELETED", await refused.Content.ReadAsStringAsync()); + + // A new bus name is a new lane; the old one goes once nothing reads it. + var renamed = $"Urgent{Guid.NewGuid():N}"[..14]; + await Api.Json(await admin.PostAsJsonAsync($"/api/workgroups/{groupId}", new + { + name = Api.Unique("Urgent"), busMessageName = renamed + })); + var newLane = Lane(groupId, renamed); + Assert.True(await EventuallyAsync(q => q.Contains(newLane) && !q.Any(n => n.StartsWith(lane))), + $"after the rename: {string.Join(", ", (await fixture.QueuesAsync()).Where(n => n.Contains($".{groupId}")))}"); + Assert.Equal(HttpStatusCode.OK, (await Api.CallSyncAsync(fixture, urlName, key)).StatusCode); + + await MoveSubscriptionAsync(null); + await Api.Json(await admin.PostAsJsonAsync($"/api/workgroups/{groupId}/delete", new { })); + Assert.True(await EventuallyAsync(q => !q.Any(n => n.StartsWith(newLane))), "the deleted group's lanes stayed"); + Assert.DoesNotContain((await Api.Json(await admin.GetAsync("/api/workgroups"))).ToJsonString(), renamed); + + // Back on the ungrouped lane, the subscription still works. + Assert.Equal(HttpStatusCode.OK, (await Api.CallSyncAsync(fixture, urlName, key)).StatusCode); + admin.Dispose(); + } + + [Fact] + public async Task A_second_work_group_with_a_name_already_taken_is_refused_and_so_is_a_rename_onto_one() + { + // Two applies running at once each made one; their subscriptions ended up split between the two. + var admin = await fixture.AdminAsync(); + var name = Api.Unique("Acme ERP"); + var first = (int)(await Api.Json(await admin.PostAsJsonAsync("/api/workgroups", new { name, busMessageName = "AcmeErp" })))["id"]!; + + var again = await admin.PostAsJsonAsync("/api/workgroups", new { name = $" {name.ToUpperInvariant()} ", busMessageName = "AcmeErp2" }); + Assert.Equal(HttpStatusCode.BadRequest, again.StatusCode); + Assert.Contains("already a work group named", await again.Content.ReadAsStringAsync()); + + var other = (int)(await Api.Json(await admin.PostAsJsonAsync("/api/workgroups", new { name = Api.Unique("Other"), busMessageName = "Other" })))["id"]!; + var rename = await admin.PostAsJsonAsync($"/api/workgroups/{other}", new { name, busMessageName = "Other" }); + Assert.Equal(HttpStatusCode.BadRequest, rename.StatusCode); + + // Saving a group under its own name is no clash. + Assert.True((await admin.PostAsJsonAsync($"/api/workgroups/{first}", new { name, busMessageName = "AcmeErp" })).IsSuccessStatusCode); + } +} diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs index 489b470a..a1e46dd8 100644 --- a/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs +++ b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.IntegrationTests.Adapters; -/// Always finds nothing — for exercising ReceivingJob's no-new-data path. +/// Always finds nothing - for exercising ReceivingJob's no-new-data path. public class NativeEmptyTestReceiver : INativeInfolinkReceiver { public string Name => nameof(NativeEmptyTestReceiver); diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs index 1a4d1d51..c71fdf2a 100644 --- a/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs +++ b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.IntegrationTests.Adapters; -/// Always fails to list files — for exercising ReceivingJob's failure path. +/// Always fails to list files - for exercising ReceivingJob's failure path. public class NativeFailingTestReceiver : INativeInfolinkReceiver { public string Name => nameof(NativeFailingTestReceiver); diff --git a/SW.Bitween.IntegrationTests/Adapters/NativePartlyFailingTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativePartlyFailingTestReceiver.cs new file mode 100644 index 00000000..e141dbc1 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativePartlyFailingTestReceiver.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; +using System.Collections.Concurrent; +using System.Threading; + +namespace SW.Bitween.IntegrationTests.Adapters; + +/// Lists three items and cannot read the middle one - for the "one bad file" path. +public class NativePartlyFailingTestReceiver : INativeInfolinkReceiver +{ + public static readonly ConcurrentBag Deleted = new(); + public static int Finalized; + + public string Name => nameof(NativePartlyFailingTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => + Task.FromResult>(["first.json", "corrupt.json", "third.json"]); + + public Task GetFile(string fileId) => fileId == "corrupt.json" + ? throw new InvalidOperationException("unreadable file") + : Task.FromResult(new XchangeFile("{}", fileId)); + + public Task DeleteFile(string fileId) + { + Deleted.Add(fileId); + return Task.CompletedTask; + } + + public Task Finalize() + { + Interlocked.Increment(ref Finalized); + return Task.CompletedTask; + } +} diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeTestResponder.cs b/SW.Bitween.IntegrationTests/Adapters/NativeTestResponder.cs index 279d0eff..cef660f7 100644 --- a/SW.Bitween.IntegrationTests/Adapters/NativeTestResponder.cs +++ b/SW.Bitween.IntegrationTests/Adapters/NativeTestResponder.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.IntegrationTests.Adapters; /// /// A delivery that answers with whatever its Body property says, marked bad when Bad -/// is true — a bad response otherwise needs an HTTP endpoint that answers 4xx. +/// is true - a bad response otherwise needs an HTTP endpoint that answers 4xx. /// public class NativeTestResponder : INativeInfolinkHandler { @@ -19,7 +19,23 @@ public class NativeTestResponder : INativeInfolinkHandler public void InitializeStartupValues(IDictionary settings) => _settings = settings; - public Task Handle(XchangeFile xchangeFile) => Task.FromResult(new XchangeFile( - _settings.TryGetValue("Body", out var body) ? body : xchangeFile.Data, - badData: _settings.TryGetValue("Bad", out var bad) && bool.Parse(bad))); + /// How many times each Body was delivered, so a test can tell a repeat delivery happened. + public static readonly System.Collections.Concurrent.ConcurrentDictionary Deliveries = new(); + + /// What each Body was handed, in order, so a test can read what a notifier sent. + public static readonly System.Collections.Concurrent.ConcurrentDictionary> Received = new(); + + public Task Handle(XchangeFile xchangeFile) + { + var hasBody = _settings.TryGetValue("Body", out var body); + if (hasBody) + { + Deliveries.AddOrUpdate(body, 1, (_, n) => n + 1); + Received.GetOrAdd(body, _ => new()).Enqueue(xchangeFile.Data); + } + + return Task.FromResult(new XchangeFile( + hasBody ? body : xchangeFile.Data, + badData: _settings.TryGetValue("Bad", out var bad) && bool.Parse(bad))); + } } diff --git a/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs b/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs index 071420b7..c53c2d2f 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs @@ -16,7 +16,8 @@ internal static class AdapterInstaller public static async Task InstallAsync(ICloudFilesService cloudFiles, string projectName, string adapterId, string entryAssembly, - IDictionary? extraMetadata = null) + IDictionary? extraMetadata = null, string? version = null, + IDictionary? extraFiles = null) { var publishDir = Path.Combine(AdaptersRoot, projectName); @@ -33,23 +34,34 @@ public static async Task InstallAsync(ICloudFilesService cloudFiles, await using var fileStream = File.OpenRead(file); await fileStream.CopyToAsync(entryStream); } + + // Files a test adds to the package - an adapter.json, typically. + foreach (var (name, content) in extraFiles ?? new Dictionary()) + { + var entry = archive.CreateEntry(name); + await using var writer = new StreamWriter(entry.Open()); + await writer.WriteAsync(content); + } } var bytes = zipStream.ToArray(); var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLower()[..16]; + var sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLower(); var metadata = new Dictionary { { "EntryAssembly", entryAssembly }, { "Hash", hash } }; + metadata["Sha256"] = sha256; foreach (var kv in extraMetadata ?? new Dictionary()) metadata[kv.Key] = kv.Value; using var uploadStream = new MemoryStream(bytes); await cloudFiles.WriteAsync(uploadStream, new WriteFileSettings { - Key = $"adapters/{adapterId}".ToLower(), + // A version is published beside the current package, as the installer does it. + Key = (version == null ? $"adapters/{adapterId}" : $"adapters-versions/{adapterId}/{version}").ToLower(), ContentType = "application/zip", Metadata = metadata }); diff --git a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs index 743014b4..aa3eb916 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs @@ -41,12 +41,15 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// Every test in the collection shares this one database, so entities must not be given hand-picked /// primary keys. Documents used to be created with literal ids chosen to be "high enough" to miss /// the seeded rows, which worked only for as long as no two test files happened to pick the same -/// number — and when they eventually did, the pair passed in isolation and failed together, which +/// number - and when they eventually did, the pair passed in isolation and failed together, which /// reads as a broken test rather than a collision. Let the database assign ids: for a document that /// is new Document(null, name, format), the constructor that exists for exactly this. /// public sealed class BitweenFixture : IAsyncLifetime { + /// The sample validator: requires a Name, and an Id from 1 to 9. + public const string SampleValidatorId = "sw.bitween.samplevalidator"; + private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build(); private readonly RabbitMqContainer _rabbitMq = new RabbitMqBuilder().Build(); @@ -128,7 +131,7 @@ public sealed class BitweenFixture : IAsyncLifetime /// Creates a queue and returns a URL that actually resolves from the test host. /// /// ElasticMQ builds QueueUrl from its own node address, which is the port INSIDE the - /// container, not the mapped one — so the URL it hands back is unreachable. Only the path is + /// container, not the mapped one - so the URL it hands back is unreachable. Only the path is /// trustworthy; the authority has to come from the mapped endpoint. /// public async Task CreateSqsQueueAsync(string name) @@ -168,7 +171,7 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star .ConfigureAppConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:RabbitMQ"] = _rabbitMq.GetConnectionString(), - // Signing material for the tokens the login handler issues. Test-only values — + // Signing material for the tokens the login handler issues. Test-only values - // the key just has to be long enough for the algorithm to accept it. ["Token:Key"] = "integration-test-signing-key-not-used-anywhere-else-0123456789", ["Token:Issuer"] = "bitween-tests", @@ -189,6 +192,9 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star // What an aggregation builds its file links on; a scheduled job has no // request to take an address from. PublicUrl = "https://bitween.test", + // The fixture is a node that runs data sources: it registers the resident + // host for them below, and resident pipeline adapters run only on such a node. + BusProvidersEnabled = true, }); services.AddSingleton(new ThemeOptions()); @@ -201,6 +207,7 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star services.AddJwtTokenParameters(); services.AddMemoryCache(); + services.AddSingleton(); services.AddScoped(); services.AddDbContext(c => @@ -220,7 +227,7 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star // Real local filesystem cloud files provider // Its OWN bucket. The default one is shared with whatever else uses the local - // store on this machine — including a developer's running Bitween — and the + // store on this machine - including a developer's running Bitween - and the // teardown below calls Cleanup(), which deletes the bucket outright. Sharing it // meant running this suite silently unpublished the dev environment's adapters, // and the next thing anyone did there failed as "metadata is missing @@ -249,13 +256,14 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - // The mapper step had no end-to-end coverage at all — every mapper test was a + // The mapper step had no end-to-end coverage at all - every mapper test was a // unit test against ScribanJsonHelper, so what XchangeService does to a payload // on the way in was never exercised. See MapperEnrichmentBaselineTests. services.AddScoped(); @@ -274,6 +282,11 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star services.AddScoped(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); @@ -281,6 +294,7 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Registration ORDER is the routing order: each runtime is asked whether an // adapter is its own, and the classic one claims everything, so it must be asked last. services.AddScoped(); @@ -296,6 +310,11 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); + // Recorded as in production; tests flush by hand (MetricsFlusher.FlushAsync) rather + // than run the minute-long loop, and the backfill is run by the tests that need it. + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(LoginServer); services.AddScoped(); services.AddScoped(); @@ -317,6 +336,8 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star var cloudFiles = scope.ServiceProvider.GetRequiredService(); await AdapterInstaller.InstallAsync(cloudFiles, "SW.Bitween.SampleHandler", "sw.bitween.samplehandler", "SW.Bitween.SampleHandler.dll"); + await AdapterInstaller.InstallAsync(cloudFiles, + "SW.Bitween.SampleValidator", SampleValidatorId, "SW.Bitween.SampleValidator.dll"); await AdapterInstaller.InstallAsync(cloudFiles, "SW.Bitween.SampleConfigurableAdapter", "sw.bitween.sampleconfigurableadapter", "SW.Bitween.SampleConfigurableAdapter.dll"); diff --git a/SW.Bitween.IntegrationTests/Fixtures/MySqlDbFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/MySqlDbFixture.cs index faab1d56..48e9e547 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/MySqlDbFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/MySqlDbFixture.cs @@ -7,14 +7,14 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// -/// A MySQL for the adapter tests to point at — deliberately its own container rather than the one +/// A MySQL for the adapter tests to point at - deliberately its own container rather than the one /// runs for the application database. /// /// A class fixture, not on the test class: xUnit builds a new instance /// of a test class per test method, so a container started there is a container per test. /// /// The seed mirrors the PostgreSQL one object for object, because the point of having both is that -/// the shared core behaves identically and the differences show up where they are real — here, a +/// the shared core behaves identically and the differences show up where they are real - here, a /// procedure that returns rows by SELECTing, which is the thing PostgreSQL cannot do. /// public class MySqlDbFixture : IAsyncLifetime @@ -44,7 +44,7 @@ public async Task InitializeAsync() // Creating a FUNCTION requires SUPER while binary logging is on, because a // non-deterministic one would make the binary log unsafe to replay. Our user is - // not SUPER and should not be, so the server is told to trust function creators — + // not SUPER and should not be, so the server is told to trust function creators - // which is the switch a DBA sets for exactly this, and is why the adapter's own // capability list does not promise that creating routines is something it can do. .WithCommand("--log-bin-trust-function-creators=1") @@ -87,7 +87,7 @@ await ExecuteAsync(connection, $"insert into {Table} (id, customer, amount) " + $"values ({i}, '{(i % 2 == 0 ? "acme" : "globex")}', {i * 10}.50)"); - // A procedure that returns rows simply by SELECTing — no cursor to declare, nothing to + // A procedure that returns rows simply by SELECTing - no cursor to declare, nothing to // bind. This is the capability PostgreSQL declares false and MySQL declares true, and the // reason the two adapters are worth testing against the same shape of schema. await ExecuteAsync(connection, $@" diff --git a/SW.Bitween.IntegrationTests/Fixtures/OracleFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/OracleFixture.cs index 9bbc22b5..c075e115 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/OracleFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/OracleFixture.cs @@ -63,7 +63,7 @@ public async Task DisposeAsync() /// /// Built here rather than taken from GetConnectionString(). Testcontainers spells the /// service name for the XE image it defaults to (XEPDB1); this image is Free, which serves - /// FREEPDB1, and the mismatch surfaces as ORA-50201 "failed to parse connect string" — which + /// FREEPDB1, and the mismatch surfaces as ORA-50201 "failed to parse connect string" - which /// says nothing about the actual cause. /// public string AdminConnectionString => diff --git a/SW.Bitween.IntegrationTests/Fixtures/PostgreSqlDbFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/PostgreSqlDbFixture.cs index 88857188..1ed25740 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/PostgreSqlDbFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/PostgreSqlDbFixture.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// -/// A PostgreSQL for the adapter tests to point at — deliberately its own container rather than the +/// A PostgreSQL for the adapter tests to point at - deliberately its own container rather than the /// one runs for the application database. /// /// Sharing that one would work and would be faster, but it would mean the adapter's test schema @@ -83,7 +83,7 @@ await ExecuteAsync(connection, $"values ({i}, '{(i % 2 == 0 ? "acme" : "globex")}', {i * 10}.50)"); // A set-returning function: PostgreSQL's answer to Oracle's REF CURSOR, and queried with - // SELECT rather than CALL — which is exactly the difference the capability list declares. + // SELECT rather than CALL - which is exactly the difference the capability list declares. await ExecuteAsync(connection, $@" create or replace function orders_by_customer(p_customer varchar) returns setof {Table} diff --git a/SW.Bitween.IntegrationTests/Fixtures/RecordingScheduleRepository.cs b/SW.Bitween.IntegrationTests/Fixtures/RecordingScheduleRepository.cs index 01f0492e..d66d22bc 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/RecordingScheduleRepository.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/RecordingScheduleRepository.cs @@ -12,7 +12,7 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// /// The handlers that create and update integrations register their schedules through this, so /// without an implementation they can't be constructed at all. A real Quartz job store would work -/// — the migrations create its tables — but it would put a background scheduler behind every test +/// - the migrations create its tables - but it would put a background scheduler behind every test /// in the collection, firing real jobs against shared state at unpredictable moments. Whether /// Quartz itself schedules correctly is a separate question, and the receiving and retry tests /// already answer it by driving the jobs directly. diff --git a/SW.Bitween.IntegrationTests/Fixtures/SqlServerDbFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/SqlServerDbFixture.cs index 75cb4928..b8ba5614 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/SqlServerDbFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/SqlServerDbFixture.cs @@ -8,14 +8,14 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// -/// A SQL Server for the adapter tests to point at — deliberately its own container rather than the +/// A SQL Server for the adapter tests to point at - deliberately its own container rather than the /// one runs for the application database. /// /// A class fixture, not on the test class: xUnit builds a new instance /// of a test class per test method, so a container started there is a container per test. /// /// The image is the 2022 developer edition, which is what Testcontainers defaults to and what runs -/// on both x64 and, through emulation, Apple silicon. It is around 1.5 GB — an order of magnitude +/// on both x64 and, through emulation, Apple silicon. It is around 1.5 GB - an order of magnitude /// less than Oracle's, so unlike those these run on every pass rather than nightly. /// public class SqlServerDbFixture : IAsyncLifetime @@ -46,7 +46,7 @@ public async Task InitializeAsync() { _container = new MsSqlBuilder() // Pinned, and not to Testcontainers' default. That default is 2019, which has no - // arm64 image and no emulation path — it exits the moment it starts on an Apple + // arm64 image and no emulation path - it exits the moment it starts on an Apple // silicon machine, which surfaces as "container is not running" rather than as // anything about the architecture. 2022 is the first release Microsoft publishes // for arm64, and it runs on both. @@ -56,7 +56,7 @@ public async Task InitializeAsync() // Wait on the PORT, not on the built-in sqlcmd probe. SQL Server listens well // before it will accept a login, and under emulation on Apple silicon the gap is // long enough that the default readiness check gives up and the container is torn - // down — which surfaces as "container is not running" rather than as a timeout. + // down - which surfaces as "container is not running" rather than as a timeout. .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433)) .Build(); @@ -122,7 +122,7 @@ async Task SeedAsync() await using var connection = new SqlConnection(AdminConnectionString); await connection.OpenAsync(); - // A schema of its own, so the discovery tests can prove the schema filter does something — + // A schema of its own, so the discovery tests can prove the schema filter does something - // everything would otherwise be dbo, where a filter that did nothing would still pass. await ExecuteAsync(connection, "create schema sales"); @@ -166,7 +166,7 @@ create procedure sales.count_orders @p_customer nvarchar(50), @p_total int outpu select @p_total = count(*) from sales.{Table} where customer = @p_customer; end"); - // An inline table-valued function — the SQL Server shape a receive statement would select + // An inline table-valued function - the SQL Server shape a receive statement would select // from, and a different object type in the catalog from a scalar one. await ExecuteAsync(connection, $@" create function sales.orders_for(@p_customer nvarchar(50)) diff --git a/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs b/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs index e7ede85e..65c73508 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/TestRequestContext.cs @@ -54,7 +54,7 @@ public static async Task AsNewViewer(this AsyncServiceScope scope, string l return viewer.Id; } - /// Signed in, but with no account behind the token — grants must resolve to nothing. + /// Signed in, but with no account behind the token - grants must resolve to nothing. public static RequestContext AsAnonymous(this AsyncServiceScope scope) { var ctx = scope.ServiceProvider.GetRequiredService(); diff --git a/SW.Bitween.IntegrationTests/NodeAdapters/orders/adapter.json b/SW.Bitween.IntegrationTests/NodeAdapters/orders/adapter.json new file mode 100644 index 00000000..df1a0be4 --- /dev/null +++ b/SW.Bitween.IntegrationTests/NodeAdapters/orders/adapter.json @@ -0,0 +1,7 @@ +{ + "id": "infolink6.handlers.nodeorders", + "version": "1.0.0", + "displayName": "Node orders", + "runtime": "node", + "entry": "main.js" +} diff --git a/SW.Bitween.IntegrationTests/NodeAdapters/orders/main.js b/SW.Bitween.IntegrationTests/NodeAdapters/orders/main.js new file mode 100644 index 00000000..9e03bb29 --- /dev/null +++ b/SW.Bitween.IntegrationTests/NodeAdapters/orders/main.js @@ -0,0 +1,21 @@ +// A Bitween handler in JavaScript (an ES module): answers an order with its settings, or rejects it as bad data. +// JavaScript rather than TypeScript so it runs on any Node CI has; SW-Serverless tests the TypeScript build. +import { expect, run, valueOf } from "@simplyworks/sw-serverless"; +import { ExchangeFile, Handler } from "@simplyworks/bitween"; + +class Orders extends Handler { + constructor() { + super(); + expect("Partner", { description: "Who receives the orders" }); + expect("Token", { secret: true, description: "The partner's key" }); + } + + handle(file) { + const order = JSON.parse(file.data); + if (order.reject) return new ExchangeFile({ data: '{"error":"rejected"}', badData: true }); + const answer = { order, to: valueOf("Partner"), token: (valueOf("Token") ?? "").length }; + return new ExchangeFile({ data: JSON.stringify(answer), filename: "answer.json" }); + } +} + +run(Orders); diff --git a/SW.Bitween.IntegrationTests/NodeAdapters/orders/package.json b/SW.Bitween.IntegrationTests/NodeAdapters/orders/package.json new file mode 100644 index 00000000..c5d4f1de --- /dev/null +++ b/SW.Bitween.IntegrationTests/NodeAdapters/orders/package.json @@ -0,0 +1,5 @@ +{ + "name": "infolink6.handlers.nodeorders", + "private": true, + "type": "module" +} diff --git a/SW.Bitween.IntegrationTests/PythonAdapters/checks/adapter.json b/SW.Bitween.IntegrationTests/PythonAdapters/checks/adapter.json new file mode 100644 index 00000000..8be7d5ca --- /dev/null +++ b/SW.Bitween.IntegrationTests/PythonAdapters/checks/adapter.json @@ -0,0 +1,6 @@ +{ + "id": "infolink6.validators.pychecks", + "version": "1.0.0", + "runtime": "python", + "entry": "main.py" +} diff --git a/SW.Bitween.IntegrationTests/PythonAdapters/checks/main.py b/SW.Bitween.IntegrationTests/PythonAdapters/checks/main.py new file mode 100644 index 00000000..7634bb2f --- /dev/null +++ b/SW.Bitween.IntegrationTests/PythonAdapters/checks/main.py @@ -0,0 +1,17 @@ +"""A Bitween validator in Python: an order needs an id.""" +import json + +import sw_serverless as sw +from simplyworks_bitween import ExchangeFile, ValidationResult, Validator + + +class Checks(Validator): + def validate(self, file: ExchangeFile) -> ValidationResult: + result = ValidationResult() + if "orderId" not in json.loads(file.data): + result.add("orderId", "An order needs an id.") + return result + + +if __name__ == "__main__": + sw.run(Checks) diff --git a/SW.Bitween.IntegrationTests/PythonAdapters/orders/adapter.json b/SW.Bitween.IntegrationTests/PythonAdapters/orders/adapter.json new file mode 100644 index 00000000..f4ed84e3 --- /dev/null +++ b/SW.Bitween.IntegrationTests/PythonAdapters/orders/adapter.json @@ -0,0 +1,8 @@ +{ + "id": "infolink6.handlers.pyorders", + "version": "1.0.0", + "displayName": "Python orders", + "summary": "Answers an order with who it went to.", + "runtime": "python", + "entry": "main.py" +} diff --git a/SW.Bitween.IntegrationTests/PythonAdapters/orders/main.py b/SW.Bitween.IntegrationTests/PythonAdapters/orders/main.py new file mode 100644 index 00000000..a7b654a8 --- /dev/null +++ b/SW.Bitween.IntegrationTests/PythonAdapters/orders/main.py @@ -0,0 +1,22 @@ +"""A Bitween handler in Python: answers an order with its settings, or rejects it as bad data.""" +import json + +import sw_serverless as sw +from simplyworks_bitween import ExchangeFile, Handler + + +class Orders(Handler): + def __init__(self): + sw.expect("Partner", description="Who receives the orders") + sw.expect("Token", secret=True, description="The partner's key") + + def handle(self, file: ExchangeFile) -> ExchangeFile: + order = json.loads(file.data) + if order.get("reject"): + return ExchangeFile(data='{"error":"rejected"}', bad_data=True) + answer = {"to": sw.value_of("Partner"), "token": len(sw.value_of("Token") or ""), "order": order} + return ExchangeFile(data=json.dumps(answer, sort_keys=True), filename="answer.json") + + +if __name__ == "__main__": + sw.run(Orders) diff --git a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj index 2ee9f77c..4dabaa37 100644 --- a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj +++ b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj @@ -8,6 +8,12 @@ + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -22,7 +28,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -43,7 +49,10 @@ - + + + @@ -59,6 +68,7 @@ + diff --git a/SW.Bitween.IntegrationTests/Tests/AccountRecoveryTests.cs b/SW.Bitween.IntegrationTests/Tests/AccountRecoveryTests.cs index 9c86b68c..1ab51801 100644 --- a/SW.Bitween.IntegrationTests/Tests/AccountRecoveryTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/AccountRecoveryTests.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// Getting a user back in when they can't sign in themselves. /// /// -/// Bitween sends no mail, so there is no self-service reset link — an administrator acting on +/// Bitween sends no mail, so there is no self-service reset link - an administrator acting on /// someone's behalf is the only route back in, which makes it worth proving it actually works /// end to end rather than assuming. These tests also pin down that resetting a password and /// clearing a lockout are two separate actions, because that is genuinely surprising: an admin who @@ -93,7 +93,7 @@ public async Task Setting_your_own_password_is_refused() scope.As(admin.Id); var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); - // Changing your own goes through ChangePassword, which demands the current one — otherwise + // Changing your own goes through ChangePassword, which demands the current one - otherwise // an unattended signed-in session is enough to take the account over permanently. var ex = await Assert.ThrowsAsync(() => handler.Handle(admin.Id, new SetAccountPasswordModel { Password = NewPassword })); diff --git a/SW.Bitween.IntegrationTests/Tests/AdapterCatalogTests.cs b/SW.Bitween.IntegrationTests/Tests/AdapterCatalogTests.cs index 8064a3b0..0ee87a22 100644 --- a/SW.Bitween.IntegrationTests/Tests/AdapterCatalogTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/AdapterCatalogTests.cs @@ -1,144 +1,352 @@ +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Fixtures; using SW.Bitween.Model; +using SW.Bitween.Services.Adapters; using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; using Xunit; namespace SW.Bitween.IntegrationTests.Tests; /// -/// Describing an adapter — which startup properties it expects — and listing a whole kind of them -/// at once. +/// Adapter manifests, the catalog and version pinning - and, first of all, that a deployment which +/// has none of them behaves exactly as it did: adapters published the old way list, describe and +/// run as before, and subscriptions that pin nothing run the current package. /// -/// -/// The published adapter here is the one the fixture uploads to local storage, so these are the -/// only tests that go through the expensive half: downloading the package and running it in a child -/// process to ask what it wants. The native adapters answer by reflection and cost nothing. -/// [Collection("Bitween")] -public class AdapterCatalogTests +public class AdapterCatalogTests(BitweenFixture fixture) { - private const string PublishedAdapter = "sw.bitween.sampleconfigurableadapter"; + const string SampleProject = "SW.Bitween.SampleHandler"; + const string SampleEntry = "SW.Bitween.SampleHandler.dll"; - private readonly BitweenFixture _fixture; + static int _seq; + static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; - public AdapterCatalogTests(BitweenFixture fixture) + /// + /// A handler published the old way - package and metadata, no manifest, no catalog - or, with + /// , also as versioned packages with a catalog entry naming the last current. + /// + async Task PublishHandler(string[] versions = null, string withdrawn = null, + string minBitween = null, List properties = null) + { + var id = $"infolink6.handlers.catalog{Interlocked.Increment(ref _seq)}"; + await using var scope = fixture.CreateScope(); + var cloudFiles = scope.ServiceProvider.GetRequiredService(); + + await AdapterInstaller.InstallAsync(cloudFiles, SampleProject, id, SampleEntry); + if (versions == null) return id; + + var entry = new AdapterCatalogEntry { Id = id }; + foreach (var version in versions) + { + var manifest = new AdapterManifest + { + Id = id, + Version = version, + DisplayName = $"Sample handler {version}", + Summary = "Echoes what it is given.", + Publisher = new AdapterPublisher { Name = "Tests" }, + Tags = ["sample"], + Kinds = ["handler"], + Entry = SampleEntry, + ReleaseNotes = $"Notes for {version}", + Properties = properties ?? [], + Compatibility = minBitween == null ? null : new AdapterCompatibility { Applications = new() { ["bitween"] = minBitween } } + }; + await AdapterInstaller.InstallAsync(cloudFiles, SampleProject, id, SampleEntry, version: version, + extraFiles: new Dictionary { [AdapterManifest.FileName] = manifest.ToJson() }); + entry.Versions.Add(new AdapterVersionRecord + { + Version = version, + PublishedOn = DateTimeOffset.UtcNow, + PublishedBy = "tests", + Manifest = manifest, + Withdrawn = version == withdrawn + }); + entry.Current = version; + entry.Manifest = manifest; + } + + await new AdapterCatalogStore(cloudFiles).SaveAsync(entry); + scope.ServiceProvider.GetRequiredService().Forget(id); + return id; + } + + async Task<(int DocumentId, int PartnerId)> Groundwork() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var document = new Document(null, Unique("Catalog doc"), DocumentFormat.Json); + var partner = new Partner(Unique("Catalog partner")); + db.AddRange(document, partner); + await db.SaveChangesAsync(); + return (document.Id, partner.Id); + } + + async Task CreateSubscription(string handlerId, string handlerVersion = null) { - _fixture = fixture; + var (documentId, partnerId) = await Groundwork(); + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var create = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + var id = (int)await create.Handle(new SubscriptionCreate + { + Name = Unique("Catalog subscription"), + DocumentId = documentId, + PartnerId = partnerId, + Type = SubscriptionType.ApiCall, + HandlerId = handlerId, + HandlerVersion = handlerVersion + }); + + // Created switched off, as every new subscription is; switched on so it can run. + await scope.ServiceProvider.GetRequiredService().Set() + .Where(s => s.Id == id).ExecuteUpdateAsync(u => u.SetProperty(s => s.Inactive, false)); + return id; } - private async Task> Describe(string adapterId) + async Task Stored(int id) { - await using var scope = _fixture.CreateScope(); + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(s => s.Id == id); + } + + async Task CatalogOf(string prefix) + { + await using var scope = fixture.CreateScope(); scope.Superuser(); - return await scope.ServiceProvider - .GetRequiredService() - .Describe(adapterId); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + return JArray.FromObject(await handler.Handle(new AdapterSearchRequest { Prefix = prefix }), + // Camel-cased property names, as the API sends them, with dictionary keys - property + // names of the adapter - left as they are. + JsonSerializer.Create(new JsonSerializerSettings + { + ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver + { + NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy + { ProcessDictionaryKeys = false } + } + })); } - private async Task Forget(string adapterId) + async Task<(string HandlerId, XchangeResult Result)> RunOnce(int subscriptionId) { - await using var scope = _fixture.CreateScope(); - scope.ServiceProvider.GetRequiredService().Forget(adapterId); + string xchangeId; + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + xchangeId = await scope.ServiceProvider.GetRequiredService() + .SubmitSubscriptionXchange(subscriptionId, new XchangeFile("{\"hello\":1}")); + } + + await using (var scope = fixture.CreateScope()) + await scope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var xchange = await db.Set().AsNoTracking().SingleAsync(x => x.Id == xchangeId); + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == xchangeId); + return (xchange.HandlerId, result); + } } + // ------------------------------------------------------------------ nothing new published + [Fact] - public async Task A_published_adapter_reports_the_values_it_expects() + public async Task An_adapter_published_the_old_way_lists_describes_and_runs_as_before() { - var described = await Describe(PublishedAdapter); + var id = await PublishHandler(); + + var listed = (await CatalogOf("handlers")).Single(a => (string)a["key"] == id); + Assert.Null((string)listed["currentVersion"]); + Assert.Null((string)listed["displayName"]); + Assert.Empty((JArray)listed["versions"]); + Assert.NotNull(listed["startupValues"]?["ContentType"]); // described by asking it, as before + + var subscription = await CreateSubscription(id); + Assert.Null((await Stored(subscription)).HandlerVersion); - Assert.Equal( - new[] { "DelayMs", "ErrorMessage", "OutputData", "SimulateError" }, - described.Keys.OrderBy(k => k).ToArray()); + var (handlerId, result) = await RunOnce(subscription); + Assert.Equal(id, handlerId); + Assert.True(result.Success, result.Exception); } - /// - /// The same instance comes back on the second ask, which is only possible from the cache — - /// running the adapter again would have built a new dictionary. - /// - /// - /// Asked from two separate scopes, because a request gets its own scope and caching that only - /// lasted the length of one would leave the catalogue exactly as slow as it was. - /// [Fact] - public async Task Describing_a_published_adapter_twice_only_runs_it_once() + public async Task Catalog_entries_are_never_listed_as_adapters() { - // From a known-cold cache, so the first ask is the one that runs the adapter however the - // other tests in this collection happened to be ordered. - await Forget(PublishedAdapter); + var id = await PublishHandler(versions: ["1.0.0", "1.1.0"]); - var first = await Describe(PublishedAdapter); - var second = await Describe(PublishedAdapter); + var keys = (await CatalogOf("handlers")).Select(a => (string)a["key"]).ToList(); - Assert.Same(first, second); + Assert.Contains(id, keys); + Assert.DoesNotContain(keys, k => k.Contains("catalog/") || k.EndsWith(".json")); + Assert.Equal(1, keys.Count(k => k == id)); } - /// - /// Several requests arriving together on a cold cache still only run the adapter once. - /// - /// - /// They all come back with the same instance, which is only possible if one of them did the - /// work and the rest waited for it — each separate run of the adapter builds its own dictionary. - /// + // ------------------------------------------------------------------ the catalog + [Fact] - public async Task Describing_the_same_adapter_from_several_requests_at_once_runs_it_once() + public async Task A_published_adapter_lists_with_its_manifest_and_history() { - await Forget(PublishedAdapter); + var id = await PublishHandler(versions: ["1.0.0", "1.1.0"], withdrawn: "1.0.0"); - var asks = Enumerable.Range(0, 8).Select(_ => Task.Run(() => Describe(PublishedAdapter))); - var results = await Task.WhenAll(asks); + var listed = (await CatalogOf("handlers")).Single(a => (string)a["key"] == id); - Assert.All(results, r => Assert.Same(results[0], r)); + Assert.Equal("1.1.0", (string)listed["currentVersion"]); + Assert.Equal("Sample handler 1.1.0", (string)listed["displayName"]); + Assert.Equal("Tests", (string)listed["publisher"]); + var history = (JArray)listed["versionHistory"]; + Assert.Equal(["1.0.0", "1.1.0"], history.Select(v => (string)v["version"])); + Assert.True((bool)history[0]["withdrawn"]); + Assert.Equal("Notes for 1.1.0", (string)history[1]["releaseNotes"]); + // The old field is still there, for the old UI - with what can be pinned, so not the withdrawn one. + Assert.Equal(["1.1.0"], ((JArray)listed["versions"]).Select(v => (string)v)); + } + + [Fact] + public async Task A_manifest_that_lists_properties_is_described_from_them() + { + var id = await PublishHandler(versions: ["1.0.0"], properties: + [ + new AdapterProperty { Name = "Endpoint", Required = true, Description = "Where to send it" }, + new AdapterProperty { Name = "Token", Secret = true, Default = "never-shown" } + ]); + + var values = (await CatalogOf("handlers")).Single(a => (string)a["key"] == id)["startupValues"]; + + Assert.NotNull(values?["Endpoint"]); + Assert.False((bool)values["Endpoint"]!["optional"]); + Assert.True((bool)values["Token"]!["private"]); + Assert.Null((string)values["Token"]!["default"]); // a secret's default is withheld, as before + Assert.Null(values["ContentType"]); // from the manifest, not by asking the adapter + } + + [Fact] + public async Task Native_adapters_list_with_their_manifest() + { + var smtp = (await CatalogOf("handlers")).Single(a => (string)a["key"] == "NativeSmtpHandler"); + Assert.Equal("Email (SMTP)", (string)smtp["displayName"]); + Assert.Equal("Simplify9", (string)smtp["publisher"]); + } + + // ------------------------------------------------------------------ pinning + + [Fact] + public async Task A_pinned_subscription_runs_its_version_and_the_exchange_remembers_it() + { + var id = await PublishHandler(versions: ["1.0.0", "1.1.0"]); + + var subscription = await CreateSubscription(id, "1.0.0"); + Assert.Equal("1.0.0", (await Stored(subscription)).HandlerVersion); + + var (handlerId, result) = await RunOnce(subscription); + Assert.Equal($"{id}/1.0.0", handlerId); + Assert.True(result.Success, result.Exception); } - /// - /// One instance is shared by every request that asks, so editing it would change what the next - /// request is told the adapter expects. - /// [Fact] - public async Task A_cached_description_cannot_be_edited() + public async Task An_unpinned_subscription_follows_the_current_version() { - var described = await Describe(PublishedAdapter); + var id = await PublishHandler(versions: ["1.0.0", "1.1.0"]); + + var subscription = await CreateSubscription(id); - Assert.Throws(() => described.Remove("DelayMs")); + var (handlerId, result) = await RunOnce(subscription); + Assert.Equal(id, handlerId); + Assert.True(result.Success, result.Exception); + } + + [Theory] + [InlineData("9.9.9")] // never published + [InlineData("1.0.0")] // withdrawn + public async Task A_version_that_cannot_run_is_refused(string version) + { + var id = await PublishHandler(versions: ["1.0.0", "1.1.0"], withdrawn: "1.0.0"); + + var error = await Assert.ThrowsAsync(() => CreateSubscription(id, version)); + Assert.Contains("ADAPTER_VERSION", error.Message); } /// - /// The catalogue answers for a whole kind in one call, properties included — the point of it - /// being that a caller drawing a form per adapter no longer asks once per adapter. + /// The request validator checks required settings before the version is checked. It used to + /// describe the unknown version by starting it, which threw - a 500 instead of the refusal. /// - /// - /// Only the native handlers are asserted. The fixture's published adapters are uploaded under - /// their own key rather than under the infolink6.handlers prefix the listing reads, so - /// they are not part of any kind's list — - /// is what covers the published path. - /// [Fact] - public async Task The_catalog_lists_a_kind_with_every_adapters_properties() + public async Task The_settings_check_leaves_an_unknown_version_to_the_version_check() { - await using var scope = _fixture.CreateScope(); - scope.Superuser(); - var handler = ActivatorUtilities - .CreateInstance(scope.ServiceProvider); + var id = await PublishHandler(versions: ["1.0.0"]); - var result = await handler.Handle(new AdapterSearchRequest { Prefix = "handlers" }); + await using var scope = fixture.CreateScope(); + var requirements = scope.ServiceProvider.GetRequiredService(); - // The handler returns anonymous types; going through JSON reads them the way the browser - // does rather than through reflection. - var rows = JArray.Parse(JsonConvert.SerializeObject(result)); + Assert.Empty(await requirements.MissingFor($"{id}/9.9.9", [])); + } - Assert.NotEmpty(rows); - Assert.All(rows, row => Assert.NotNull(row["StartupValues"])); + [Fact] + public async Task A_built_in_adapter_cannot_be_pinned() + { + var error = await Assert.ThrowsAsync(() => + CreateSubscription("NativeHttpHandler", "1.0.0")); + Assert.Contains("ADAPTER_VERSION", error.Message); + } + + [Fact] + public async Task A_version_without_a_catalog_can_be_pinned_when_its_package_exists() + { + // Published as versions by an installer that wrote no catalog. + var id = await PublishHandler(); + await using (var scope = fixture.CreateScope()) + await AdapterInstaller.InstallAsync(scope.ServiceProvider.GetRequiredService(), + SampleProject, id, SampleEntry, version: "2.0.0"); + + var subscription = await CreateSubscription(id, "2.0.0"); + + var (handlerId, result) = await RunOnce(subscription); + Assert.Equal($"{id}/2.0.0", handlerId); + Assert.True(result.Success, result.Exception); + } + + [Fact] + public async Task A_version_an_older_installer_published_lists_and_can_be_pinned() + { + // Where the installer put versions before they had a prefix of their own - beside the + // current package, under adapters/{id}/. (Only the version, here: a file-system store + // can't also hold adapters/{id} as a file.) + var id = $"infolink6.handlers.oldlayout{Interlocked.Increment(ref _seq)}"; + await using (var scope = fixture.CreateScope()) + { + var cloudFiles = scope.ServiceProvider.GetRequiredService(); + await AdapterInstaller.InstallAsync(cloudFiles, SampleProject, $"{id}/1.2.0", SampleEntry); + } + + var listed = (await CatalogOf("handlers")).Single(a => (string)a["key"] == id); + Assert.Equal(["1.2.0"], ((JArray)listed["versions"]).Select(v => (string)v)); + + var subscription = await CreateSubscription(id, "1.2.0"); + var (handlerId, result) = await RunOnce(subscription); + Assert.Equal($"{id}/1.2.0", handlerId); + Assert.True(result.Success, result.Exception); + } + + [Fact] + public async Task An_adapter_needing_a_newer_bitween_is_refused_when_chosen_but_not_held_against_a_save() + { + var id = await PublishHandler(versions: ["1.0.0"], minBitween: "99.0.0"); - // The properties arrive with the list rather than needing a request each, which is the - // whole reason this endpoint exists. - var smtp = rows.Single(r => r["Key"]!.ToString() == "NativeSmtpHandler"); - Assert.True(smtp["Native"]!.Value()); - Assert.NotEmpty(smtp["StartupValues"]!.Children()); + var error = await Assert.ThrowsAsync(() => CreateSubscription(id)); + Assert.Contains("ADAPTER_NEEDS_NEWER_BITWEEN", error.Message); + Assert.Contains("99.0.0", error.Message); } } diff --git a/SW.Bitween.IntegrationTests/Tests/AdapterEditorTests.cs b/SW.Bitween.IntegrationTests/Tests/AdapterEditorTests.cs new file mode 100644 index 00000000..f821131d --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/AdapterEditorTests.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Resources.AdapterDrafts; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// The adapter editor, end to end on real storage: a Python or JavaScript adapter started from the +/// template, built and checked against the Bitween contract, tried, published as a version that is +/// not current, made current, and started again from what was published - with what it refuses, +/// who may do it, and the audit trail. +/// +[Collection("Bitween")] +public class AdapterEditorTests(BitweenFixture fixture) +{ + static int _seq; + static string Unique(string prefix) => $"{prefix}{Interlocked.Increment(ref _seq)}{Guid.NewGuid().ToString("N")[..6]}"; + + static readonly Dictionary Settings = new() { ["ApiKey"] = "k" }; + + async Task As(Func> act) + { + await using var scope = fixture.CreateScope(); + scope.Superuser(); + return await act(scope.ServiceProvider); + } + + Task NewDraft(string name, string language = "python", string kind = "handler") => + As(async sp => (int)await ActivatorUtilities.CreateInstance(sp) + .Handle(new AdapterDraftCreate { Name = name, Language = language, Kind = kind })); + + Task Draft(int id) => + As(sp => sp.GetRequiredService().Set().AsNoTracking().SingleAsync(d => d.Id == id)); + + Task Build(int id, Dictionary settings = null) => + As(async sp => JObject.FromObject(await ActivatorUtilities.CreateInstance(sp) + .Handle(id, new AdapterDraftRun { Settings = settings ?? Settings }))); + + async Task Save(int id, Func, Dictionary> change) + { + var files = change((await Draft(id)).Files); + await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftUpdate { Files = files })); + } + + [Theory] + [InlineData("python")] + [InlineData("node")] + public async Task A_new_adapter_builds_conforms_and_can_be_tried(string language) + { + var name = Unique(language == "python" ? "PyEdit" : "JsEdit"); + var id = await NewDraft(name, language); + var draft = await Draft(id); + Assert.Equal(language, draft.Language); + Assert.Contains(language == "python" ? "main.py" : "main.js", draft.Files.Keys); + + var built = await Build(id); + Assert.True((bool)built["Succeeded"], built.ToString()); + Assert.True((bool)built["Conforms"], built.ToString()); + Assert.Contains(built["Settings"]!, s => (string)s["Name"] == "ApiKey" && (bool)s["Secret"]); + Assert.Equal(["Handle"], built["Commands"]!.Select(c => (string)c).ToArray()); + + var tried = await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftRun + { + Settings = Settings, Command = "Handle", Input = "{\"Data\":\"hello\",\"Filename\":\"a.txt\"}", + })); + var run = (WorkshopRun)tried; + Assert.True(run.Succeeded, run.Error ?? string.Join("; ", run.Problems)); + Assert.Equal("hello", (string)JObject.Parse(run.Output)["Data"]); + } + + [Fact] + public async Task Checks_run_without_a_required_setting_say_so() + { + var id = await NewDraft(Unique("PyNoKey")); + var built = await Build(id, new Dictionary()); + Assert.True((bool)built["Succeeded"], built.ToString()); + Assert.Contains(built["Warnings"]!, w => ((string)w!).StartsWith("ApiKey is required but was empty")); + } + + [Fact] + public async Task An_edit_that_breaks_the_adapter_says_why() + { + var id = await NewDraft(Unique("PyBroken")); + await Save(id, files => { files["main.py"] = files["main.py"].Replace("def handle(", "def handle(self,"); return files; }); + + var built = await Build(id); + Assert.False((bool)built["Succeeded"]); + Assert.Contains("SyntaxError", string.Join(" ", built["Problems"]!.Select(p => (string)p))); + } + + [Fact] + public async Task What_the_editor_cannot_build_is_refused_with_the_reason() + { + var id = await NewDraft(Unique("PyDeps")); + await Save(id, files => { files["requirements.txt"] += "\nrequests==2.32.3\n"; return files; }); + var built = await Build(id); + Assert.False((bool)built["Succeeded"]); + Assert.Contains("requests", (string)built["Problems"]![0]); + + var other = await NewDraft(Unique("PyRenamed")); + await Save(other, files => { files["adapter.json"] = files["adapter.json"].Replace("\"id\": \"", "\"id\": \"someone.else."); return files; }); + Assert.Contains("this draft is for", (string)(await Build(other))["Problems"]![0]); + + foreach (var path in new[] { "../escape.py", "/abs.py", "a\\\\b.py", "a/../b.py" }) + await Assert.ThrowsAsync(() => Save(other, files => { files[path] = "x"; return files; })); + } + + [Fact] + public async Task A_save_over_someone_else_s_is_refused_unless_it_says_to() + { + var id = await NewDraft(Unique("PyTwo")); + var opened = await Draft(id); + + // Someone else saves first, from the same version. + await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftUpdate + { + Files = opened.Files.ToDictionary(f => f.Key, f => f.Value + (f.Key == "main.py" ? "\n# theirs\n" : "")), + BaseHash = opened.FilesHash, + })); + + var mine = opened.Files.ToDictionary(f => f.Key, f => f.Value + (f.Key == "main.py" ? "\n# mine\n" : "")); + var refused = await Assert.ThrowsAsync(() => + As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftUpdate { Files = mine, BaseHash = opened.FilesHash }))); + Assert.Contains("after you opened it", refused.Message + string.Join(" ", refused.Validations.Select(v => v.Value))); + Assert.Contains("# theirs", (await Draft(id)).Files["main.py"]); + + // Saving over theirs, knowingly: without a base, as a client from before this does. + await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftUpdate { Files = mine })); + Assert.Contains("# mine", (await Draft(id)).Files["main.py"]); + } + + [Fact] + public async Task A_try_that_uses_too_much_memory_is_stopped_and_says_so() + { + var id = await NewDraft(Unique("PyHog")); + await Save(id, files => + { + // Holds on to more and more, as a leak would, until something stops it. + files["main.py"] = files["main.py"].Replace("import sw_serverless as sw", + "import time\nimport sw_serverless as sw\n_held = []\n\ndef _hog():\n for _ in range(120):\n _held.append(bytearray(8 * 1024 * 1024))\n time.sleep(0.05)\n"); + files["main.py"] = System.Text.RegularExpressions.Regex.Replace(files["main.py"], + @"(def handle\([^)]*\)[^:]*:\n)", "$1 _hog()\n"); + return files; + }); + + var options = fixture.App.Services.GetRequiredService(); + var memory = options.AdapterEditorMemoryMb; + options.AdapterEditorMemoryMb = 150; + try + { + var run = (WorkshopRun)await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftRun + { + Settings = Settings, Command = "Handle", Input = "{\"Data\":\"x\",\"Filename\":\"a.txt\"}", + })); + Assert.False(run.Succeeded); + Assert.Contains("memory limit of 150 MB", run.Error ?? string.Join("; ", run.Problems)); + } + finally + { + options.AdapterEditorMemoryMb = memory; + } + } + + async Task WithDependencies(Func> act) + { + var options = fixture.App.Services.GetRequiredService(); + options.AdapterEditorDependencies = true; + try { return await act(); } + finally { options.AdapterEditorDependencies = false; } + } + + [Fact] + public async Task With_dependencies_on_only_plain_names_and_versions_are_fetched() + { + var py = await NewDraft(Unique("PySources")); + await Save(py, files => + { + files["requirements.txt"] += "\n--index-url https://example.com/simple\nhttps://example.com/x-1.0-py3-none-any.whl\n" + + "./local\nthing @ git+https://example.com/thing.git\n"; + return files; + }); + var problems = (await WithDependencies(() => Build(py)))["Problems"]!.Select(p => (string)p).ToList(); + Assert.Equal(4, problems.Count(p => p.Contains("isn't a package name and version"))); + + var js = await NewDraft(Unique("JsSources"), "node"); + await Save(js, files => + { + var package = System.Text.Json.Nodes.JsonNode.Parse(files["package.json"])!.AsObject(); + package["dependencies"] = new System.Text.Json.Nodes.JsonObject + { + ["local"] = "file:../../etc", ["remote"] = "git+https://example.com/r.git", ["alias"] = "npm:left-pad@1.3.0", + ["fine"] = "^7.0.0", + }; + files["package.json"] = package.ToJsonString(); + return files; + }); + problems = (await WithDependencies(() => Build(js)))["Problems"]!.Select(p => (string)p).ToList(); + Assert.Equal(3, problems.Count(p => p.Contains("isn't a package and version range"))); + Assert.DoesNotContain(problems, p => p.Contains("\"fine\"")); + } + + [SkippableFact] + public async Task With_dependencies_on_a_package_from_the_index_is_vendored_and_runs() + { + try + { + using var probe = new System.Net.Sockets.TcpClient(); + await probe.ConnectAsync("pypi.org", 443).WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (Exception ex) + { + Skip.If(true, $"PyPI can't be reached from here: {ex.GetBaseException().Message}"); + } + + var id = await NewDraft(Unique("PySix")); + await Save(id, files => + { + files["requirements.txt"] += "\nsix==1.16.0\n"; + files["main.py"] = "import six\n" + files["main.py"]; + return files; + }); + + Assert.False((bool)(await Build(id))["Succeeded"]); + var built = await WithDependencies(() => Build(id)); + Assert.True((bool)built["Succeeded"], built.ToString()); + Assert.True((bool)built["Conforms"], built.ToString()); + } + + [Fact] + public async Task Publishing_makes_a_version_that_is_not_current_until_it_is_promoted() + { + var name = Unique("PyPublish"); + var id = await NewDraft(name); + var adapterId = (await Draft(id)).AdapterId; + + var published = JObject.FromObject(await As(sp => ActivatorUtilities.CreateInstance(sp) + .Handle(id, new AdapterDraftPublish { Version = "1.0.0", ReleaseNotes = "First", Settings = Settings }))); + Assert.True((bool)published["Published"], published.ToString()); + Assert.Equal("1.0.0", (string)published["Version"]); + + var entry = await As(sp => sp.GetRequiredService().GetAsync(adapterId)); + Assert.Null(entry.Current); + var version = entry.Find("1.0.0"); + Assert.Equal("First", version.Manifest.ReleaseNotes); + Assert.Equal("python", version.Manifest.Runtime); + Assert.Contains("main.py", version.Manifest.Source.Files.Keys); + Assert.Equal("1.0.0", (await Draft(id)).BaseVersion); + + // A version can't be published twice; the next one is a patch on top. + var again = JObject.FromObject(await As(sp => ActivatorUtilities.CreateInstance(sp) + .Handle(id, new AdapterDraftPublish { Settings = Settings }))); + Assert.Equal("1.0.1", (string)again["Version"]); + + await As(sp => ActivatorUtilities.CreateInstance(sp) + .Handle(new Resources.Adapters.AdapterPromoteRequest { AdapterId = adapterId, Version = "1.0.0" })); + Assert.Equal("1.0.0", (await As(sp => sp.GetRequiredService().GetAsync(adapterId))).Current); + + var releases = await As(sp => sp.GetRequiredService().Set().AsNoTracking() + .Where(r => r.AdapterId == adapterId).OrderBy(r => r.Id).Select(r => r.Action + " " + r.Version).ToListAsync()); + Assert.Equal(["published 1.0.0", "published 1.0.1", "promoted 1.0.0"], releases); + + var audited = await As(sp => sp.GetRequiredService().Set().AsNoTracking() + .Where(e => e.EntityName == nameof(AdapterDraft) && e.EntityKey == id.ToString()).ToListAsync()); + Assert.NotEmpty(audited); + Assert.DoesNotContain(audited, e => (e.Changes ?? "").Contains("def handle")); + } + + [Fact] + public async Task A_draft_can_start_from_a_published_version_s_source() + { + var id = await NewDraft(Unique("PyAgain")); + await Save(id, files => { files["main.py"] = files["main.py"].Replace("Send file.data", "Send the file's data"); return files; }); + var original = await Draft(id); + await As(sp => ActivatorUtilities.CreateInstance(sp) + .Handle(id, new AdapterDraftPublish { Version = "2.0.0", Settings = Settings })); + + var copy = await As(async sp => (int)await ActivatorUtilities.CreateInstance(sp) + .Handle(new AdapterDraftCreate { FromAdapterId = original.AdapterId, FromVersion = "2.0.0" })); + var started = await Draft(copy); + + Assert.Equal("python", started.Language); + Assert.Equal("handler", started.Kind); + Assert.Equal("2.0.0", started.BaseVersion); + Assert.Equal(original.Files["main.py"], started.Files["main.py"]); + } + + [Fact] + public async Task A_name_already_published_is_edited_from_a_version_not_started_again() + { + var name = Unique("PyTaken"); + var id = await NewDraft(name); + await As(sp => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftPublish { Settings = Settings })); + + await Assert.ThrowsAsync(() => NewDraft(name)); + } + + [Fact] + public async Task Without_the_permission_nothing_is_built_published_or_promoted() + { + var id = await NewDraft(Unique("PyGuarded")); + + await using var scope = fixture.CreateScope(); + await scope.AsNewViewer($"editor-viewer-{Guid.NewGuid():N}"); + var sp = scope.ServiceProvider; + + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(sp).Handle(id)); + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftRun())); + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(sp).Handle(id, new AdapterDraftPublish())); + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(sp) + .Handle(new Resources.Adapters.AdapterPromoteRequest { AdapterId = "x", Version = "1.0.0" })); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/AdapterSourceTests.cs b/SW.Bitween.IntegrationTests/Tests/AdapterSourceTests.cs new file mode 100644 index 00000000..34f84142 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/AdapterSourceTests.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Resources.Adapters; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; +using SW.Serverless.Contract.Catalog; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Reading the source a published version carries: listed from the catalog, read from the package, +/// checked against the manifest's hashes, guarded, and audited - and nothing for a version that +/// carries none, which is every version published before source was. +/// +[Collection("Bitween")] +public class AdapterSourceTests(BitweenFixture fixture) +{ + static int _seq; + + static string Sha(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + + /// + /// Publishes each version as serverless build packages it: binaries at the root, source under + /// source/, the manifest listing each source file's hash. A null source publishes without one. + /// + async Task Publish(params (string Version, Dictionary Source, Dictionary Recorded)[] versions) + { + var id = $"infolink6.handlers.source{Interlocked.Increment(ref _seq)}"; + await using var scope = fixture.CreateScope(); + var cloudFiles = scope.ServiceProvider.GetRequiredService(); + + var entry = new AdapterCatalogEntry { Id = id }; + foreach (var (version, source, recorded) in versions) + { + var manifest = new AdapterManifest + { + Id = id, Version = version, Kinds = ["handler"], Entry = "Adapter.dll", + Language = "csharp", Runtime = AdapterManifest.DotnetRuntime + }; + if (source != null) + manifest.Source = new AdapterSource + { + BuildCommand = "dotnet publish -c Release", + Files = recorded ?? source.ToDictionary(s => s.Key, s => Sha(s.Value)) + }; + + using var zip = new MemoryStream(); + using (var archive = new ZipArchive(zip, ZipArchiveMode.Create, leaveOpen: true)) + { + void Add(string name, byte[] bytes) + { + using var stream = archive.CreateEntry(name).Open(); + stream.Write(bytes); + } + + Add("Adapter.dll", [0x4d, 0x5a, 0, 0]); + Add(AdapterManifest.FileName, Encoding.UTF8.GetBytes(manifest.ToJson())); + foreach (var (path, bytes) in source ?? []) Add($"source/{path}", bytes); + } + + zip.Position = 0; + await cloudFiles.WriteAsync(zip, new WriteFileSettings + { + Key = AdapterCatalogPaths.Version("adapters", id, version), + ContentType = "application/zip" + }); + entry.Versions.Add(new AdapterVersionRecord { Version = version, PublishedOn = DateTimeOffset.UtcNow, Manifest = manifest }); + entry.Current = version; + entry.Manifest = manifest; + } + + await new AdapterCatalogStore(cloudFiles).SaveAsync(entry); + scope.ServiceProvider.GetRequiredService().Forget(id); + return id; + } + + static Dictionary Files(params (string Path, string Text)[] files) => + files.ToDictionary(f => f.Path, f => Encoding.UTF8.GetBytes(f.Text)); + + [Fact] + public async Task Lists_EachVersionsFiles_WithTheirHashes() + { + var id = await Publish( + ("1.0.0", Files(("Handler.cs", "class A {}"), ("Adapter.csproj", "")), null), + ("1.1.0", Files(("Handler.cs", "class A { int x; }"), ("Adapter.csproj", ""), ("Util.cs", "static class U {}")), null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + var v1 = (AdapterSourceListing)await handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0" }); + var v2 = (AdapterSourceListing)await handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.1.0" }); + + Assert.Equal(["Adapter.csproj", "Handler.cs"], v1.Files.Select(f => f.Path)); + Assert.Equal(["Adapter.csproj", "Handler.cs", "Util.cs"], v2.Files.Select(f => f.Path)); + Assert.Equal("dotnet publish -c Release", v1.BuildCommand); + Assert.Equal("csharp", v1.Language); + + // What a diff of the two shows as changed, without reading a file. + var changed = v2.Files.Where(f => v1.Files.FirstOrDefault(o => o.Path == f.Path)?.Sha256 != f.Sha256).Select(f => f.Path); + Assert.Equal(["Handler.cs", "Util.cs"], changed); + } + + [Fact] + public async Task ReadsAFile_AndAuditsTheRead() + { + var id = await Publish(("1.0.0", Files(("src/Handler.cs", "class Handler {}\n")), null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + var file = (AdapterSourceFile)await handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0", Path = "src/Handler.cs" }); + + Assert.Equal("class Handler {}\n", file.Content); + Assert.False(file.Binary); + + var db = scope.ServiceProvider.GetRequiredService(); + var access = await db.Set().AsNoTracking().SingleAsync(a => a.AdapterId == id); + Assert.Equal("1.0.0", access.Version); + Assert.Equal("src/Handler.cs", access.Path); + + // In the trail, beside every configuration change. + Assert.True(await db.Set().AsNoTracking() + .AnyAsync(e => e.EntityName == nameof(AdapterSourceAccess) && e.EntityKey == access.Id.ToString())); + } + + [Fact] + public async Task ABinaryFile_IsListedButNotShown() + { + var id = await Publish(("1.0.0", new Dictionary { ["icon.png"] = [0x89, 0x50, 0x4e, 0x47, 0, 1, 2] }, null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var file = (AdapterSourceFile)await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0", Path = "icon.png" }); + + Assert.True(file.Binary); + Assert.Null(file.Content); + Assert.Equal(7, file.Size); + } + + [Fact] + public async Task AFileThatDoesNotMatchItsHash_IsRefused() + { + var id = await Publish(("1.0.0", Files(("Handler.cs", "class Tampered {}")), + new Dictionary { ["Handler.cs"] = Sha(Encoding.UTF8.GetBytes("class Original {}")) })); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0", Path = "Handler.cs" })); + } + + [Fact] + public async Task OnlyListedFiles_CanBeRead() + { + var id = await Publish(("1.0.0", Files(("Handler.cs", "class A {}")), null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + // The package's binaries and manifest are in the zip, but are not source. + foreach (var path in new[] { "../Adapter.dll", "Adapter.dll", "../adapter.json", "Missing.cs", null }) + await Assert.ThrowsAsync(() => + handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0", Path = path })); + } + + [Fact] + public async Task AVersionWithoutSource_ListsNothing() + { + var id = await Publish(("1.0.0", null, null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var listing = (AdapterSourceListing)await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0" }); + + Assert.Empty(listing.Files); + Assert.Null(listing.BuildCommand); + } + + [Fact] + public async Task AnUnknownVersionOrAdapter_IsNotFound() + { + var id = await Publish(("1.0.0", Files(("Handler.cs", "class A {}")), null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "9.9.9" })); + await Assert.ThrowsAsync(() => handler.Handle(new AdapterSourceRequest { AdapterId = id, Version = "latest" })); + await Assert.ThrowsAsync(() => handler.Handle(new AdapterSourceRequest { AdapterId = "nobody.here", Version = "1.0.0" })); + } + + [Fact] + public async Task AnAdapterOnlyInTheCatalog_IsListedUnderItsKinds() + { + // Every adapter in another runtime than .NET is published this way: versions and catalog, + // never adapters/{id}, which older hosts would run with dotnet. + var id = await Publish(("1.0.0", Files(("main.py", "def handle(x): return x")), null)); + + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var listing = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + var keys = async (string prefix) => ((System.Collections.IEnumerable)await listing.Handle(new Model.AdapterSearchRequest { Prefix = prefix })) + .Cast().Select(o => (string)o.GetType().GetProperty("Key")!.GetValue(o)!).ToList(); + + Assert.Contains(id, await keys("handlers")); + Assert.DoesNotContain(id, await keys("receivers")); + + // Listed once, however many places it is found. + Assert.Single(await keys("handlers"), k => k == id); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AnAdapterInAnotherRuntime_IsDescribedFromItsManifest_NeverByStartingIt(bool declaresProperties) + { + // No package at all: were it started to be asked, describing would fail or, against a real + // gRPC-only adapter, wait out the describer's timeout. + var id = $"infolink6.handlers.python{Interlocked.Increment(ref _seq)}"; + var manifest = new AdapterManifest + { + Id = id, Version = "1.0.0", Kinds = ["handler"], Entry = "main.py", Runtime = AdapterManifest.PythonRuntime, + Properties = declaresProperties ? [new AdapterProperty { Name = "Token", Required = true, Secret = true }] : [] + }; + await using var scope = fixture.CreateScope(); + await new AdapterCatalogStore(scope.ServiceProvider.GetRequiredService()).SaveAsync(new AdapterCatalogEntry + { + Id = id, Current = "1.0.0", Manifest = manifest, + Versions = [new AdapterVersionRecord { Version = "1.0.0", PublishedOn = DateTimeOffset.UtcNow, Manifest = manifest }] + }); + scope.ServiceProvider.GetRequiredService().Forget(id); + + var started = DateTime.UtcNow; + var values = await scope.ServiceProvider.GetRequiredService().Describe(id); + + Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(5)); + if (declaresProperties) + Assert.True(Assert.Single(values).Value.Private); + else + Assert.Empty(values); + } + + [Fact] + public async Task AnAdapterWhoseVersionsAreAllWithdrawn_IsNotListed() + { + var id = await Publish(("1.0.0", Files(("main.py", "x")), null)); + await using var scope = fixture.CreateScope(); + var store = new AdapterCatalogStore(scope.ServiceProvider.GetRequiredService()); + var entry = await store.GetAsync(id); + entry.Versions[0].Withdrawn = true; + await store.SaveAsync(entry); + scope.ServiceProvider.GetRequiredService().Forget(id); + + scope.Superuser(); + var rows = (System.Collections.IEnumerable)await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new Model.AdapterSearchRequest { Prefix = "handlers" }); + Assert.DoesNotContain(rows.Cast(), o => (string)o.GetType().GetProperty("Key")!.GetValue(o)! == id); + } + + [Fact] + public async Task AViewer_CannotReadSource() + { + var id = await Publish(("1.0.0", Files(("Handler.cs", "class A {}")), null)); + + await using var scope = fixture.CreateScope(); + await scope.AsNewViewer($"source-viewer-{Interlocked.Increment(ref _seq)}"); + + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0" })); + await Assert.ThrowsAsync(() => ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new AdapterSourceRequest { AdapterId = id, Version = "1.0.0", Path = "Handler.cs" })); + + Assert.False(await scope.ServiceProvider.GetRequiredService() + .Set().AnyAsync(a => a.AdapterId == id)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs b/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs index 6e2bc637..415e042c 100644 --- a/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs @@ -86,7 +86,7 @@ public async Task Aggregation_job_skips_already_aggregated_xchanges() db.Set().Add(sourceSub); await db.SaveChangesAsync(); - // Create 2 source Xchanges — one will be pre-aggregated, one will not + // Create 2 source Xchanges - one will be pre-aggregated, one will not var x1 = new Xchange(sourceSub, new XchangeFile("{\"seq\":1}")); var x2 = new Xchange(sourceSub, new XchangeFile("{\"seq\":2}")); db.Set().Add(x1); @@ -182,7 +182,7 @@ public async Task A_run_that_rolls_something_up_records_the_exchange_it_made() var aggSub = new Subscription("Agg Attempt", sourceSub.Id, Partner.SystemId); aggSub.Inactive = false; - // A real aggregation always has one — SetSchedules throws without it, and the run would + // A real aggregation always has one - SetSchedules throws without it, and the run would // then report a failure it did not have. aggSub.SetSchedules([new Schedule(Recurrence.Daily, TimeSpan.FromHours(2))]); db.Set().Add(aggSub); @@ -200,6 +200,52 @@ public async Task A_run_that_rolls_something_up_records_the_exchange_it_made() Assert.Equal([rollUp.Id], attempt.ExchangeIds); } + [Fact] + public async Task Two_replicas_running_one_aggregation_at_once_make_one_roll_up_and_no_failure() + { + // Two schedules, or a schedule and "aggregate now", firing together: separate Quartz jobs, so + // nothing in Quartz stops them overlapping. + int aggId; + await using (var setup = fixture.CreateScope()) + { + var db = setup.ServiceProvider.GetRequiredService(); + var sourceDoc = new Document(null, "Agg Two Replicas Doc", DocumentFormat.Json); + db.Set().Add(sourceDoc); + await db.SaveChangesAsync(); + var sourceSub = new Subscription("Agg Two Replicas Source", sourceDoc.Id) { Inactive = false }; + db.Set().Add(sourceSub); + await db.SaveChangesAsync(); + for (var i = 0; i < 5; i++) + { + var xchange = new Xchange(sourceSub, new XchangeFile($"{{\"i\":{i}}}")); + db.Set().Add(xchange); + await db.SaveChangesAsync(); + db.Set().Add(new XchangeResult(xchange.Id, null, null)); + await db.SaveChangesAsync(); + } + var aggSub = new Subscription("Agg Two Replicas", sourceSub.Id, Partner.SystemId) { Inactive = false }; + aggSub.SetSchedules([new Schedule(Recurrence.Daily, TimeSpan.FromHours(2))]); + db.Set().Add(aggSub); + await db.SaveChangesAsync(); + aggId = aggSub.Id; + } + fixture.App.Services.GetRequiredService().Revoke(); + + await using var first = fixture.CreateScope(); + await using var second = fixture.CreateScope(); + await Task.WhenAll( + first.ServiceProvider.GetRequiredService().Execute(new AggregationJobParams(aggId, null)), + second.ServiceProvider.GetRequiredService().Execute(new AggregationJobParams(aggId, null))); + + await using var check = fixture.CreateScope(); + var read = check.ServiceProvider.GetRequiredService(); + Assert.Equal(1, await read.Set().CountAsync(x => x.SubscriptionId == aggId)); + Assert.DoesNotContain(await read.Set().Where(a => a.SubscriptionId == aggId).Select(a => a.Outcome).ToListAsync(), + o => o == ReceiveOutcome.Failed); + // And the flag is let go: the next run goes ahead, finding nothing left. + Assert.False((await read.Set().AsNoTracking().SingleAsync(s => s.Id == aggId)).IsRunning); + } + [Fact] public async Task A_run_with_nothing_outstanding_records_no_new_data_rather_than_nothing() { @@ -211,7 +257,7 @@ public async Task A_run_with_nothing_outstanding_records_no_new_data_rather_than var sourceDoc = new Document(null, "Agg Empty Attempt Doc", DocumentFormat.Json); db.Set().Add(sourceDoc); await db.SaveChangesAsync(); - // A source with no exchanges at all — the run has nothing to collect. + // A source with no exchanges at all - the run has nothing to collect. var sourceSub = new Subscription("Agg Empty Attempt Source", sourceDoc.Id); sourceSub.Inactive = false; db.Set().Add(sourceSub); @@ -219,7 +265,7 @@ public async Task A_run_with_nothing_outstanding_records_no_new_data_rather_than var aggSub = new Subscription("Agg Empty Attempt", sourceSub.Id, Partner.SystemId); aggSub.Inactive = false; - // A real aggregation always has one — SetSchedules throws without it, and the run would + // A real aggregation always has one - SetSchedules throws without it, and the run would // then report a failure it did not have. aggSub.SetSchedules([new Schedule(Recurrence.Daily, TimeSpan.FromHours(2))]); db.Set().Add(aggSub); @@ -232,7 +278,7 @@ public async Task A_run_with_nothing_outstanding_records_no_new_data_rather_than var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == aggSub.Id); Assert.Equal(ReceiveOutcome.NoNewData, attempt.Outcome); Assert.Empty(attempt.ExchangeIds); - // The quiet outcome still creates no exchange — that part has not changed. + // The quiet outcome still creates no exchange - that part has not changed. Assert.Equal(0, await db.Set().CountAsync(x => x.SubscriptionId == aggSub.Id)); } @@ -250,7 +296,7 @@ public async Task An_inactive_aggregation_records_no_run_at_all() db.Set().Add(sourceSub); await db.SaveChangesAsync(); - // Inactive by default — the job returns before it does anything, and a run that never + // Inactive by default - the job returns before it does anything, and a run that never // happened must not appear in the history as a quiet success. var aggSub = new Subscription("Agg Skipped Attempt", sourceSub.Id, Partner.SystemId); db.Set().Add(aggSub); @@ -382,7 +428,7 @@ public async Task The_list_reports_when_an_aggregation_next_runs() var row = response.Result.Single(r => r.Id == id); // ReceiveOn is only ever set for Receiving, and the list page reads one next-run field - // for both scheduled types — so without AggregateOn in the projection every aggregation + // for both scheduled types - so without AggregateOn in the projection every aggregation // row showed no next run at all, for a job that plainly has one. Assert.NotNull(row.AggregateOn); Assert.Null(row.ReceiveOn); diff --git a/SW.Bitween.IntegrationTests/Tests/ApiGatewayTests.cs b/SW.Bitween.IntegrationTests/Tests/ApiGatewayTests.cs index bc273ff4..3a90c873 100644 --- a/SW.Bitween.IntegrationTests/Tests/ApiGatewayTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ApiGatewayTests.cs @@ -151,7 +151,7 @@ public async Task A_partner_key_sent_any_other_way_is_refused(string how) "bearer-wrong-key" => $"Bearer {Guid.NewGuid():N}", "bearer-empty" => "Bearer ", // The password alone would find the partner, but a username naming some other key - // means the client is set up wrong — better refused now than accepted by accident. + // means the client is set up wrong - better refused now than accepted by accident. "basic-wrong-username" => Basic("orders-test", key), "basic-key-as-username" => Basic(key, ""), "basic-not-base64" => "Basic not*base64", @@ -233,7 +233,7 @@ public async Task A_key_header_that_would_be_read_as_something_else_is_refused(s /// /// A gateway that trusts the test login server, with one partner attached whose identity - /// there is returned — plus a second partner with an identity of its own, not attached. + /// there is returned - plus a second partner with an identity of its own, not attached. /// private async Task<(string urlName, string identity, string strangerIdentity)> JwtGateway() { @@ -296,7 +296,7 @@ public async Task A_jwt_gateway_refuses_any_other_token(string how) var token = how switch { "expired" => login.Token(identity, expires: DateTime.UtcNow.AddMinutes(-10)), - // Signed by the right login server, but issued for some other system — the audience + // Signed by the right login server, but issued for some other system - the audience // is the only thing that keeps it out. "for-another-system" => login.Token(identity, audience: "payroll"), "from-another-login-server" => login.Token(identity, issuer: "https://login.elsewhere.test"), @@ -433,7 +433,7 @@ public async Task Two_partners_cannot_share_a_login_identity() } [Theory] - [InlineData("order sync")] // the one that actually happens — a space + [InlineData("order sync")] // the one that actually happens - a space [InlineData("Order-Sync")] // upper case, which the route match is not [InlineData("-orders")] [InlineData("/orders")] // an empty part, front or back or middle @@ -445,7 +445,7 @@ public async Task Two_partners_cannot_share_a_login_identity() public async Task A_url_name_that_cannot_appear_in_a_path_is_refused(string urlName) { // Partners call /api/gateway/{urlName}/sync. Anything needing escaping there produces a - // gateway that reads as configured on every screen and cannot be reached — and the URL + // gateway that reads as configured on every screen and cannot be reached - and the URL // the partner is given to copy is the broken one. var ex = await Assert.ThrowsAsync(() => CreateGateway(urlName)); Assert.StartsWith("GATEWAY_URL_NAME_INVALID", ex.Message); @@ -470,7 +470,7 @@ public async Task A_usable_url_name_is_accepted(string urlName) public async Task A_url_name_matching_an_older_mixed_case_one_is_taken() { // Rows saved before names had to be lowercase can still hold capitals, and partner calls - // match without regard to case — so the lowercase twin would answer on the same address. + // match without regard to case - so the lowercase twin would answer on the same address. var legacy = Unique("Legacy-Orders"); await using (var scope = fixture.CreateScope()) { @@ -493,7 +493,7 @@ public async Task Attaching_a_partner_demands_an_integration_of_the_gateway_kind await using (var scope = fixture.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); - // A perfectly good integration — of a kind that is started by its own schedule, not + // A perfectly good integration - of a kind that is started by its own schedule, not // by a partner calling in. var receiving = new Subscription(Unique("Scheduled"), documentId); db.Set().Add(receiving); @@ -515,7 +515,7 @@ public async Task The_same_partner_and_integration_cannot_be_attached_twice() var attachment = new ApiGatewayPartnerCreate { PartnerId = partnerId, SubscriptionId = subscriptionId }; await AddPartner(gatewayId, attachment); - // Without this the second attachment wins silently and the first is unreachable — two + // Without this the second attachment wins silently and the first is unreachable - two // rows on screen where only one can ever run. await Assert.ThrowsAsync(() => AddPartner(gatewayId, attachment)); } @@ -540,7 +540,7 @@ public async Task Deleting_a_gateway_takes_its_attachments_with_it() Assert.False(await db.Set().AnyAsync(g => g.Id == gatewayId)); Assert.False(await db.Set().AnyAsync(p => p.ApiGatewayId == gatewayId)); - // The integration itself is configuration in its own right and outlives the gateway — + // The integration itself is configuration in its own right and outlives the gateway - // cascading into it would delete work the attachment merely referenced. Assert.True(await db.Set().AnyAsync(s => s.Id == subscriptionId)); } @@ -628,7 +628,7 @@ public async Task An_inline_gateway_integration_cannot_carry_its_own_partner() var (partnerId, documentId, _) = await Groundwork(); var gatewayId = await CreateGateway(Unique("gw").ToLowerInvariant()); - // The partner reaches a gateway integration through the attachment — which is the very + // The partner reaches a gateway integration through the attachment - which is the very // thing being made here. One on the integration too is a second, disagreeing answer to // the same question. var ex = await Assert.ThrowsAsync(() => AddPartner(gatewayId, diff --git a/SW.Bitween.IntegrationTests/Tests/AuditTrailTests.cs b/SW.Bitween.IntegrationTests/Tests/AuditTrailTests.cs index 3217f5cf..d53bd61c 100644 --- a/SW.Bitween.IntegrationTests/Tests/AuditTrailTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/AuditTrailTests.cs @@ -12,7 +12,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// The audit trail, end to end against a real database: that configuration changes are recorded, -/// that runtime traffic isn't, and — the part worth being sure of — that no credential ever +/// that runtime traffic isn't, and - the part worth being sure of - that no credential ever /// reaches the table. /// [Collection("Bitween")] @@ -150,7 +150,7 @@ public async Task An_account_password_never_reaches_the_trail() /// /// Settings are the one place a value's secrecy is known without asking an adapter, so unlike - /// the adapter property bags they are recorded — except where the catalog says otherwise. + /// the adapter property bags they are recorded - except where the catalog says otherwise. /// [Fact] public async Task A_secret_settings_value_is_redacted_while_a_plain_one_is_kept() @@ -184,8 +184,8 @@ static Dictionary Changes(AuditEntry entry) => p => new Diff { Old = Text(p.Value["Old"]), New = Text(p.Value["New"]) }); /// - /// A value as the test compares it. Most columns are recorded as text, but a collection — the - /// names of a partner's secret properties, say — is recorded as the list it is. + /// A value as the test compares it. Most columns are recorded as text, but a collection - the + /// names of a partner's secret properties, say - is recorded as the list it is. /// static string? Text(Newtonsoft.Json.Linq.JToken? value) => value?.Type switch { diff --git a/SW.Bitween.IntegrationTests/Tests/AutoPauseTests.cs b/SW.Bitween.IntegrationTests/Tests/AutoPauseTests.cs new file mode 100644 index 00000000..6422b2a4 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/AutoPauseTests.cs @@ -0,0 +1,209 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// A subscription set to pause after some number of failed deliveries in a row stops sending to a +/// target that is down, and holds what arrives meanwhile. One not set to never does. +/// +[Collection("Bitween")] +public class AutoPauseTests(BitweenFixture fixture) +{ + private const string Responder = nameof(Adapters.NativeTestResponder); + private const string Missing = "native:no-such-handler"; + + private static int _seq; + private static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; + + private async Task NewSubscription(int? autoPauseAfter, int? retryPolicyId = null) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var doc = new Document(null, Unique("Auto-pause type"), DocumentFormat.Json); + db.Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription(Unique("Auto-pause sub"), doc.Id, SubscriptionType.GatewayApiCall) + { Inactive = false, HandlerId = Missing, AutoPauseAfterFailures = autoPauseAfter }; + if (retryPolicyId != null) sub.SetRetryPolicy(retryPolicyId, null); + db.Add(sub); + await db.SaveChangesAsync(); + return sub.Id; + } + + private async Task SetHandler(int subscriptionId, string handlerId) + { + await using var scope = fixture.CreateScope(); + await scope.ServiceProvider.GetRequiredService().Set() + .Where(s => s.Id == subscriptionId) + .ExecuteUpdateAsync(u => u.SetProperty(s => s.HandlerId, handlerId)); + } + + /// Submits one exchange to the subscription and processes it. + private async Task Deliver(int subscriptionId) + { + string xchangeId; + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + xchangeId = await scope.ServiceProvider.GetRequiredService() + .SubmitSubscriptionXchange(subscriptionId, new XchangeFile("{}")); + } + + await using (var scope = fixture.CreateScope()) + { + await scope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + } + + return xchangeId; + } + + private async Task Reload(int subscriptionId) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(s => s.Id == subscriptionId); + } + + private async Task PauseEvents(int subscriptionId) + { + await using var scope = fixture.CreateScope(); + var bodies = await scope.ServiceProvider.GetRequiredService().Set() + .AsNoTracking().Where(m => m.MessageType == nameof(SubscriptionAutoPausedEvent)) + .Select(m => m.Body).ToListAsync(); + return bodies.Count(b => JsonConvert.DeserializeObject(b)!.SubscriptionId == subscriptionId); + } + + [Fact] + public async Task A_subscription_not_set_to_pause_never_does() + { + var id = await NewSubscription(null); + + for (var i = 0; i < 3; i++) await Deliver(id); + + var sub = await Reload(id); + Assert.Null(sub.PausedOn); + Assert.Equal(0, sub.ConsecutiveFailures); + } + + [Fact] + public async Task It_pauses_on_the_failure_that_reaches_the_limit_and_says_so_once() + { + var id = await NewSubscription(2); + + await Deliver(id); + var afterOne = await Reload(id); + Assert.Null(afterOne.PausedOn); + Assert.Equal(1, afterOne.ConsecutiveFailures); + Assert.NotNull(afterOne.LastException); + + await Deliver(id); + var afterTwo = await Reload(id); + Assert.NotNull(afterTwo.PausedOn); + Assert.True(afterTwo.PausedAutomatically); + + // Already paused: a later failure doesn't pause it again or send a second alert. + await Deliver(id); + Assert.Equal(afterTwo.PausedOn, (await Reload(id)).PausedOn); + Assert.Equal(1, await PauseEvents(id)); + } + + [Fact] + public async Task A_delivery_that_goes_through_starts_the_count_again() + { + var id = await NewSubscription(2); + + await Deliver(id); + await SetHandler(id, Responder); + await Deliver(id); + Assert.Equal(0, (await Reload(id)).ConsecutiveFailures); + + await SetHandler(id, Missing); + await Deliver(id); + + var sub = await Reload(id); + Assert.Null(sub.PausedOn); + Assert.Equal(1, sub.ConsecutiveFailures); + } + + [Fact] + public async Task Resuming_after_an_automatic_pause_starts_the_count_again() + { + var id = await NewSubscription(1); + await Deliver(id); + Assert.True((await Reload(id)).PausedAutomatically); + + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(id, new SubscriptionPause()); + } + + var sub = await Reload(id); + Assert.Null(sub.PausedOn); + Assert.False(sub.PausedAutomatically); + Assert.Equal(0, sub.ConsecutiveFailures); + Assert.Null(sub.LastException); + } + + [Fact] + public async Task The_pause_is_sent_to_the_retry_policys_alert_handler() + { + int policyId; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var policy = new RetryPolicy { Name = Unique("Auto-pause policy"), AlertHandlerId = Responder }; + db.Add(policy); + await db.SaveChangesAsync(); + policyId = policy.Id; + } + + var id = await NewSubscription(1, policyId); + var xchangeId = await Deliver(id); + + await using (var scope = fixture.CreateScope()) + { + await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Process(new SubscriptionAutoPausedEvent + { SubscriptionId = id, XchangeId = xchangeId, Failures = 1, OccurredOn = DateTime.UtcNow }); + } + + await using (var scope = fixture.CreateScope()) + { + var sent = await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking() + .SingleAsync(n => n.XchangeId == xchangeId && n.NotifierName == XchangeNotification.AutoPauseAlertName); + Assert.True(sent.Success, sent.Exception); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1001)] + public async Task A_limit_outside_one_to_a_thousand_is_refused(int limit) + { + var id = await NewSubscription(null); + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var get = (SubscriptionGet)await ActivatorUtilities + .CreateInstance(scope.ServiceProvider).Handle(id); + get.AutoPauseAfterFailures = limit; + + await Assert.ThrowsAsync(() => ActivatorUtilities + .CreateInstance(scope.ServiceProvider).Handle(id, get)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/BusGatewayRouteTests.cs b/SW.Bitween.IntegrationTests/Tests/BusGatewayRouteTests.cs index d681e747..9be3d633 100644 --- a/SW.Bitween.IntegrationTests/Tests/BusGatewayRouteTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/BusGatewayRouteTests.cs @@ -13,13 +13,13 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// Wiring up a bus gateway's routes — the configuration side of what +/// Wiring up a bus gateway's routes - the configuration side of what /// exercises at run time. /// /// /// A bus gateway is bound to one information type and every route under it inherits that binding. /// The rules here exist because the alternative is a route that saves cleanly and then never fires -/// — its integration reads a payload shaped like something else, matches nothing, and reports no +/// - its integration reads a payload shaped like something else, matches nothing, and reports no /// error at any point. /// [Collection("Bitween")] @@ -103,7 +103,7 @@ public async Task A_route_demands_an_integration_of_the_bus_gateway_kind() await using (var scope = fixture.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); - // Right information type, wrong trigger — this one is started by its own schedule. + // Right information type, wrong trigger - this one is started by its own schedule. var receiving = new Subscription(Unique("Scheduled"), documentId); db.Set().Add(receiving); await db.SaveChangesAsync(); @@ -138,7 +138,7 @@ public async Task An_integration_defined_inline_takes_the_gateways_information_t await AddRoute(gatewayId, new BusGatewayRouteCreate { // DocumentId is deliberately wrong here. A bus gateway is bound to one information - // type and imposes it, so the caller's answer is not consulted — which is also why + // type and imposes it, so the caller's answer is not consulted - which is also why // the mismatch this could otherwise cause is not reachable through this door. NewIntegration = new InlineIntegrationCreate { Name = integrationName, DocumentId = 999_999 }, }); @@ -174,7 +174,7 @@ async Task Update(BusGatewayRouteUpdate model) await handler.Handle(gatewayId, model); } - // The information type check has to hold on the edit path too — otherwise every rule + // The information type check has to hold on the edit path too - otherwise every rule // above is one save-and-edit away from being bypassed. await Assert.ThrowsAsync(() => Update(new BusGatewayRouteUpdate { RouteId = routeId, SubscriptionId = mismatched })); diff --git a/SW.Bitween.IntegrationTests/Tests/BusProviderSupervisorTests.cs b/SW.Bitween.IntegrationTests/Tests/BusProviderSupervisorTests.cs index 74e20dd0..f2b21e4c 100644 --- a/SW.Bitween.IntegrationTests/Tests/BusProviderSupervisorTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/BusProviderSupervisorTests.cs @@ -28,7 +28,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// Everything else in the external-bus feature has been exercised through an adapter started by /// hand. That skips the part most likely to go wrong in production, because reconciliation is not a /// one-shot: it runs every thirty seconds, for ever, against a database other people are editing. -/// The failures it can produce are the expensive kind — an adapter restarted on every pass drops +/// The failures it can produce are the expensive kind - an adapter restarted on every pass drops /// in-flight messages, a lease held after it was superseded means two nodes consuming one queue, /// and one unreachable broker taking the loop down with it stops every other integration on the /// node. @@ -41,7 +41,7 @@ public class BusProviderSupervisorTests(BitweenFixture fixture) { /// /// The whole point, end to end: a row in the database becomes a live broker connection, and a - /// message on that broker becomes an Xchange — with nobody starting an adapter by hand. + /// message on that broker becomes an Xchange - with nobody starting an adapter by hand. /// [Fact] public async Task Reconciling_starts_the_adapter_for_an_active_data_source() @@ -113,7 +113,7 @@ public async Task A_second_gateway_adds_its_queue_without_losing_the_first() /// The most consequential thing this loop does is NOTHING. Reconciliation runs every thirty /// seconds; if an unchanged data source were restarted each pass, every long-running consumer /// on the node would be torn down twice a minute and whatever it held in flight would be - /// redelivered. The process id is the honest witness — a restart cannot preserve it. + /// redelivered. The process id is the honest witness - a restart cannot preserve it. /// [Fact] public async Task An_unchanged_data_source_is_left_alone_across_passes() @@ -175,7 +175,7 @@ public async Task Deactivating_a_data_source_stops_its_adapter() } /// - /// Stopping is not enough — the lease has to go too. A node holding a lock on a data source + /// Stopping is not enough - the lease has to go too. A node holding a lock on a data source /// nobody is running would block whichever node later wants it, and nothing would ever /// release it. /// @@ -202,7 +202,7 @@ public async Task Deactivating_a_data_source_releases_its_lease() /// /// Placement. A data source another node already owns must not be started here, or both nodes - /// consume the same queue and every message is processed twice — the failure this whole design + /// consume the same queue and every message is processed twice - the failure this whole design /// exists to prevent. /// [Fact] @@ -226,16 +226,16 @@ public async Task A_data_source_owned_by_another_node_is_not_started_here() /// lease to be superseded still believes it holds ownership; the database term is what tells it /// otherwise. Bumping the term stands in for that pause. /// - /// What the supervisor must do is STOP — immediately, and without draining, because another + /// What the supervisor must do is STOP - immediately, and without draining, because another /// node may already be consuming and finishing in-flight work here would process the same /// messages twice. The process id is the witness: it can only change if the adapter was torn /// down and started again. /// /// It then starts again under a new term, and that is correct rather than a miss. Losing the - /// term does not mean the resource is taken — it means this node's claim is no longer proof + /// term does not mean the resource is taken - it means this node's claim is no longer proof /// that it isn't. So it drops everything and asks again. Here nobody else holds the broker /// lock, so it legitimately wins; when another node really does hold it, the re-acquire fails - /// and the adapter stays down — which is what + /// and the adapter stays down - which is what /// covers. /// [Fact] @@ -273,7 +273,7 @@ public async Task A_superseded_lease_stops_the_adapter_before_anything_else() /// /// The handover this node is on the losing side of. Once another node holds the resource, an - /// adapter still running here is duplicate processing — so it has to go, even though the data + /// adapter still running here is duplicate processing - so it has to go, even though the data /// source is perfectly active and perfectly healthy. /// /// StopAsync releases the leases but leaves the supervisor's idea of what it is running intact, @@ -318,7 +318,7 @@ public async Task One_data_source_that_cannot_start_does_not_stop_the_others() await using var supervisor = Supervisor(); - // Not "does not throw" — the pass has to complete far enough to start the good one. + // Not "does not throw" - the pass has to complete far enough to start the good one. await supervisor.ReconcileAsync(); Assert.Null(InstanceOf(broken)); @@ -328,6 +328,35 @@ public async Task One_data_source_that_cannot_start_does_not_stop_the_others() Assert.NotNull(await WaitForXchangeAsync(documentId)); } + /// + /// A node that cannot start a data source - it can't reach the broker, say - used to keep the + /// lease and retry forever, so the node that could have run it never got the chance. After a + /// few failed starts it lets go. + /// + [Fact] + public async Task A_data_source_that_keeps_failing_to_start_gives_up_its_lease() + { + var broken = await CreateDataSourceAsync(adapterId: "bitween.bus.doesnotexist"); + await AddGatewayAsync(broken, Unique("sup-yield")); + + await using var supervisor = Supervisor(); + await supervisor.ReconcileAsync(); + + using var other = Node(); + Assert.Null(await other.TryAcquireAsync($"datasource.{broken}")); + + await supervisor.ReconcileAsync(); + await supervisor.ReconcileAsync(); + + var lease = await WaitForAcquireAsync(other, $"datasource.{broken}"); + Assert.NotNull(lease); + + // And while it waits out the pause, this node does not grab it back. + await supervisor.ReconcileAsync(); + Assert.Null(InstanceOf(broken)); + await lease!.DisposeAsync(); + } + /// /// Health has to land in the database, because that is the only place the UI and the notifiers /// can see it. An operator should not need to tail logs to find out a broker went away. @@ -383,7 +412,7 @@ public async Task Stopping_the_supervisor_releases_what_it_held() /// /// An inactive gateway must not contribute its endpoint. Otherwise deactivating a gateway would - /// leave Bitween still consuming its queue and acking messages nobody routes anywhere — a + /// leave Bitween still consuming its queue and acking messages nobody routes anywhere - a /// silent drop, which is worse than an error. /// [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/BusTests.cs b/SW.Bitween.IntegrationTests/Tests/BusTests.cs index 9e861073..d681fe40 100644 --- a/SW.Bitween.IntegrationTests/Tests/BusTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/BusTests.cs @@ -47,6 +47,6 @@ public async Task Can_publish_multiple_messages_in_sequence() { await publish.Publish("TestSequenceEvent", $"{{\"seq\":{i}}}"); } - // Passes if no exception is thrown — confirms the channel stays open + // Passes if no exception is thrown - confirms the channel stays open } } diff --git a/SW.Bitween.IntegrationTests/Tests/CacheRevocationTests.cs b/SW.Bitween.IntegrationTests/Tests/CacheRevocationTests.cs index 412bef05..c6d16ad9 100644 --- a/SW.Bitween.IntegrationTests/Tests/CacheRevocationTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/CacheRevocationTests.cs @@ -19,7 +19,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Announcing means BroadcastRevoke(), which publishes rather than clearing directly: each /// instance owns a private queue bound to the shared node exchange, so every instance clears its -/// own copy — including the one that handled the write. The fixture deliberately does not call +/// own copy - including the one that handled the write. The fixture deliberately does not call /// AddBusConsume, so that round trip cannot complete here and the announcement is asserted /// at the handler instead, with a cache that records the call. /// diff --git a/SW.Bitween.IntegrationTests/Tests/CarriedFormatTests.cs b/SW.Bitween.IntegrationTests/Tests/CarriedFormatTests.cs index b7a28192..2fe6066a 100644 --- a/SW.Bitween.IntegrationTests/Tests/CarriedFormatTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/CarriedFormatTests.cs @@ -26,7 +26,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Clients build custom adapters that send whatever format they like, and Bitween has no path /// syntax for most of them. These types say so: no promoted properties, and no filter that could -/// pass — but everything that asked for the type without a filter still gets every message. +/// pass - but everything that asked for the type without a filter still gets every message. /// JSON types are deliberately left as they were, where a payload the reader can't open reaches /// nobody; clients run live setups on that, and switching the type is how they opt in. /// @@ -44,7 +44,7 @@ private record Wiring(int DocumentId, int Unfiltered, int Filtered, int RoutedUn /// /// A type promoting nothing, with one of each kind of listener: Internal subscriptions and bus /// routes, with and without a filter. The filters would pass on a JSON payload of - /// {"country":"JO"}, so a skip means the payload wasn't read — not that it didn't match. + /// {"country":"JO"}, so a skip means the payload wasn't read - not that it didn't match. /// private async Task Arrange(DocumentFormat format) { diff --git a/SW.Bitween.IntegrationTests/Tests/DataSourceApiTests.cs b/SW.Bitween.IntegrationTests/Tests/DataSourceApiTests.cs index 48161c95..c4995500 100644 --- a/SW.Bitween.IntegrationTests/Tests/DataSourceApiTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DataSourceApiTests.cs @@ -24,8 +24,8 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Until this existed the whole external-bus feature was reachable only by writing rows into the /// database by hand, which is how every other test in this suite still sets itself up. The -/// questions worth asking of it are about credentials — a broker password must not come back out -/// of an endpoint, and it must survive an edit that never touched it — and about the two ways a +/// questions worth asking of it are about credentials - a broker password must not come back out +/// of an endpoint, and it must survive an edit that never touched it - and about the two ways a /// gateway can be wrong: pointed at nothing, or pointed at a queue another gateway already reads. /// [Collection("Bitween")] @@ -91,16 +91,39 @@ public async Task Saving_a_masked_secret_back_keeps_the_stored_value() var id = await CreateAsync(new Dictionary { ["Host"] = "broker.example.com", - ["Password"] = "hunter2" + ["Password"] = "hunter2", + ["Prefetch"] = "16" }); var row = await GetAsync(id); - row.Properties["Host"] = "broker2.example.com"; // the only real edit + row.Properties["Prefetch"] = "32"; // the only real edit await UpdateAsync(id, row); Assert.Equal("hunter2", await StoredPropertyAsync(id, "Password")); - Assert.Equal("broker2.example.com", await StoredPropertyAsync(id, "Host")); + Assert.Equal("32", await StoredPropertyAsync(id, "Prefetch")); + } + + /// + /// Except when the edit points the connection somewhere else. Kept across that, the password + /// would go to whoever runs the new host, sent there by someone who never saw it. + /// + [Fact] + public async Task Pointing_the_connection_elsewhere_drops_the_masked_secret() + { + var id = await CreateAsync(new Dictionary + { + ["Host"] = "broker.example.com", + ["Password"] = "hunter2" + }); + + var row = await GetAsync(id); + row.Properties["Host"] = "broker.attacker.example"; + + await UpdateAsync(id, row); + + Assert.Null(await StoredPropertyAsync(id, "Password")); + Assert.Equal("broker.attacker.example", await StoredPropertyAsync(id, "Host")); } /// And a genuine change still goes through, or the field would be uneditable. @@ -117,8 +140,8 @@ public async Task A_secret_that_was_actually_changed_is_saved() } /// - /// A data source pasted from a Get response — which is how anyone scripts a second environment - /// — must not authenticate with the sentinel. There is nothing stored to restore from, so it + /// A data source pasted from a Get response - which is how anyone scripts a second environment + /// - must not authenticate with the sentinel. There is nothing stored to restore from, so it /// is dropped and the field reads as unset rather than as a password nobody chose. /// [Fact] @@ -260,7 +283,7 @@ public async Task An_external_gateway_without_an_endpoint_is_refused() /// /// Two gateways on one endpoint would both be candidates for every message and only one would - /// ever run — a silent misroute rather than an error, which is the failure mode this whole + /// ever run - a silent misroute rather than an error, which is the failure mode this whole /// area keeps producing when it is left unguarded. /// [Fact] @@ -314,7 +337,7 @@ public async Task A_gateway_can_be_moved_from_internal_to_external_and_back() /// /// The ceilings have to reach the adapter's process, not just the database. /// - /// They are applied at launch — a GC heap hard limit on the child process — so the supervisor + /// They are applied at launch - a GC heap hard limit on the child process - so the supervisor /// has to fold them into the fingerprint it compares each pass. Otherwise raising a limit /// saves cleanly, changes nothing, and the adapter keeps running under the old one with /// nothing to say it did not take. @@ -329,7 +352,7 @@ public async Task Changing_a_memory_ceiling_restarts_the_adapter() var host = fixture.App.Services.GetRequiredService(); // Disposed with the test: the election holds the exclusive queue that IS the lock, so - // leaking one leaves this data source owned by a node that no longer exists — and every + // leaking one leaves this data source owned by a node that no longer exists - and every // later reconcile quietly declines to start it. using var election = Node(); var supervisor = Supervisor(host, election); @@ -405,7 +428,7 @@ public async Task Changing_the_cpu_ceiling_restarts_the_adapter() var host = fixture.App.Services.GetRequiredService(); // Disposed with the test: the election holds the exclusive queue that IS the lock, so - // leaking one leaves this data source owned by a node that no longer exists — and every + // leaking one leaves this data source owned by a node that no longer exists - and every // later reconcile quietly declines to start it. using var election = Node(); var supervisor = Supervisor(host, election); @@ -440,7 +463,7 @@ public async Task Changing_the_cpu_ceiling_restarts_the_adapter() } /// - /// The CPU figure is a share of the whole node, so anything above 100 can never be reached — + /// The CPU figure is a share of the whole node, so anything above 100 can never be reached - /// the ceiling would sit there looking configured and never fire once. /// [Fact] @@ -497,7 +520,7 @@ public async Task Inspecting_a_connection_this_node_does_not_hold_says_so() /// /// A data source is not only a broker: a resident adapter holding a database session is one /// too, and one of those has no queue for a gateway to consume. Refusing it here matters more - /// than the menu that hides it — the menu is a courtesy, this is the rule. + /// than the menu that hides it - the menu is a courtesy, this is the rule. /// [Fact] public async Task A_bus_gateway_cannot_read_from_a_non_broker_data_source() @@ -566,7 +589,27 @@ private async Task GetAsync(int id) return (DataSourceRow)await handler.Handle(id); } - private async Task UpdateAsync(int id, DataSourceRow row) + /// + /// Placement was reset to Auto by any save that left it out, which the admin UI always did: + /// renaming an Exclusive or PerNode source moved where it ran. + /// + [Fact] + public async Task A_save_that_leaves_placement_out_keeps_it() + { + var id = await CreateAsync(new Dictionary()); + await UpdateAsync(id, await GetAsync(id), placement: "PerNode"); + Assert.Equal("PerNode", (await GetAsync(id)).Placement.ToString()); + + var renamed = await GetAsync(id); + renamed.Name = Unique("renamed"); + await UpdateAsync(id, renamed); + Assert.Equal("PerNode", (await GetAsync(id)).Placement.ToString()); + + await UpdateAsync(id, await GetAsync(id), placement: "Auto"); + Assert.Equal("Auto", (await GetAsync(id)).Placement.ToString()); + } + + private async Task UpdateAsync(int id, DataSourceRow row, string placement = null) { await using var scope = fixture.CreateScope(); scope.Superuser(); @@ -583,7 +626,8 @@ private async Task UpdateAsync(int id, DataSourceRow row) SoftMemoryLimitMb = row.SoftMemoryLimitMb, HardMemoryLimitMb = row.HardMemoryLimitMb, CpuPercentLimit = row.CpuPercentLimit, - CpuLimitSamples = row.CpuLimitSamples + CpuLimitSamples = row.CpuLimitSamples, + Placement = placement }); } diff --git a/SW.Bitween.IntegrationTests/Tests/DataSourceProviderCatalogTests.cs b/SW.Bitween.IntegrationTests/Tests/DataSourceProviderCatalogTests.cs index 59ebf600..bfabee39 100644 --- a/SW.Bitween.IntegrationTests/Tests/DataSourceProviderCatalogTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DataSourceProviderCatalogTests.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// The front end used to carry this itself: which fields a RabbitMQ connection starts with, what /// DeclareMode accepts, which properties are credentials. A hand-kept copy of someone else's -/// contract drifts, and it did — the form advertised a "Tls" setting the adapter never read, so an +/// contract drifts, and it did - the form advertised a "Tls" setting the adapter never read, so an /// operator could set it, see nothing wrong, and still be authenticating in the clear. /// /// These tests are about that failure mode. They assert the description comes from the adapter @@ -28,7 +28,7 @@ public class DataSourceProviderCatalogTests(BitweenFixture fixture) { /// /// The adapters the fixture installed describe themselves. Nothing in Bitween names these - /// settings — delete the attributes from RabbitOptions and this list empties. + /// settings - delete the attributes from RabbitOptions and this list empties. /// [Fact] public async Task A_provider_describes_its_own_settings() @@ -63,7 +63,7 @@ public async Task Allowed_values_come_from_the_adapter() } /// - /// The setting whose absence started this. It must be named UseSsl — what the adapter binds — + /// The setting whose absence started this. It must be named UseSsl - what the adapter binds - /// and not the "Tls" the old hand-written hint told operators to add. /// [Fact] @@ -77,7 +77,7 @@ public async Task Tls_is_described_by_the_name_the_adapter_actually_binds() /// /// A credential is declared as one by the adapter, so the form masks it before anything has - /// been saved — without the name having to match a heuristic. + /// been saved - without the name having to match a heuristic. /// [Fact] public async Task A_credential_is_declared_secret_by_the_adapter() @@ -104,7 +104,7 @@ public async Task Host_supplied_settings_are_not_offered_as_fields() } /// - /// A number is described as one. Coarse on purpose — it decides which input to render, and + /// A number is described as one. Coarse on purpose - it decides which input to render, and /// anything finer would be the UI knowing about brokers again. /// [Fact] @@ -141,7 +141,7 @@ public async Task The_resource_lists_every_installed_provider() /// /// An adapter that is not a provider has nothing to describe, and asking about one that was - /// never installed is not an error either — a broken or absent package must not empty the menu + /// never installed is not an error either - a broken or absent package must not empty the menu /// for the ones that are fine. /// [Fact] @@ -154,7 +154,7 @@ public async Task An_adapter_that_declares_nothing_is_simply_not_a_provider() /// /// A provider nobody at Simplify9 published. Bitween's own adapters are found by their - /// "bitween." id prefix, which cannot possibly find a third party's — so the installer stamps + /// "bitween." id prefix, which cannot possibly find a third party's - so the installer stamps /// what an adapter IS onto the package (SW-Serverless #117, from its [AdapterKind]), and the /// catalog reads that instead of the name. /// diff --git a/SW.Bitween.IntegrationTests/Tests/DataSourceStatementTests.cs b/SW.Bitween.IntegrationTests/Tests/DataSourceStatementTests.cs index cad79b0d..ac8909de 100644 --- a/SW.Bitween.IntegrationTests/Tests/DataSourceStatementTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DataSourceStatementTests.cs @@ -17,12 +17,12 @@ namespace SW.Bitween.IntegrationTests.Tests; /// SQL statements as their own entity. /// /// They have to live on the connection rather than on a subscription, because a subscription's -/// adapter properties have partner values templated into them before the adapter sees them — SQL +/// adapter properties have partner values templated into them before the adapter sees them - SQL /// there would be an injection surface fed by ordinary partner data. But keeping them in a field on /// the data source meant that writing a query needed the same right as changing the credentials. /// /// So: their own entity, their own permission, their own audit trail, and a usage count. These -/// tests cover the four things that separation is supposed to buy — namespacing, safe deletion, +/// tests cover the four things that separation is supposed to buy - namespacing, safe deletion, /// knowing what uses what, and the adapter contract staying exactly as it was. /// [Collection("Bitween")] @@ -32,7 +32,7 @@ public class DataSourceStatementTests(BitweenFixture fixture) /// /// A collision is an error at save time, not a silent overwrite. Case-insensitively, because - /// the adapter resolves names that way — allowing getOrder and GetOrder to coexist would make + /// the adapter resolves names that way - allowing getOrder and GetOrder to coexist would make /// which one runs a matter of dictionary ordering. /// [Fact] @@ -104,7 +104,7 @@ public async Task Usage_reports_the_subscriptions_that_name_the_statement() } /// - /// Zero usage is the interesting number — the only way to tell dead SQL from SQL that is + /// Zero usage is the interesting number - the only way to tell dead SQL from SQL that is /// merely quiet, which is what stopped anyone cleaning up the JSON blob this replaced. /// [Fact] @@ -120,7 +120,7 @@ public async Task An_unused_statement_reports_no_usage() /// /// A subscription bound to ANOTHER data source that happens to use the same statement name is - /// not a user of this one — statements are scoped to their connection. + /// not a user of this one - statements are scoped to their connection. /// [Fact] public async Task Usage_does_not_count_a_subscription_on_a_different_data_source() @@ -140,7 +140,7 @@ await CreateSubscriptionAsync(theirs, // ---------------------------------------------------------------- safe change /// - /// Deleting out from under a live subscription does not fail at delete time — it fails on the + /// Deleting out from under a live subscription does not fail at delete time - it fails on the /// next message, as "not a statement this data source defines", somewhere nobody is watching. /// So it is refused here, and the refusal names what is still using it. /// @@ -174,7 +174,7 @@ public async Task Deleting_an_unused_statement_works() } /// - /// A rename breaks every subscription naming the old one, and nothing here can fix that — the + /// A rename breaks every subscription naming the old one, and nothing here can fix that - the /// subscription's properties are its own. Changing the SQL is fine; changing the name is not. /// [Fact] @@ -195,7 +195,7 @@ await CreateSubscriptionAsync(dataSourceId, Assert.Contains("cannot be renamed while", error.Message); - // The SQL itself is editable, which is the common case — a column was added, a join fixed. + // The SQL itself is editable, which is the common case - a column was added, a join fixed. await UpdateAsync(statementId, new DataSourceStatementUpdate { Name = "keepThisName", @@ -259,7 +259,7 @@ async Task CreateStatementAsync(int dataSourceId, string name, string sql) var handler = ActivatorUtilities.CreateInstance( scope.ServiceProvider); - // The handler answers with { Id, Checked } — the id, and whether the database actually + // The handler answers with { Id, Checked } - the id, and whether the database actually // looked at the SQL. Read through the property rather than cast, because the shape is an // anonymous type and a cast to int is what this used to do. var created = await handler.Handle( @@ -304,7 +304,7 @@ async Task CreateSubscriptionAsync(int dataSourceId, await using var scope = fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - // Document names are capped at 100 and subscription names at 100, so a raw guid fits — + // Document names are capped at 100 and subscription names at 100, so a raw guid fits - // the earlier attempt to trim to 40 was slicing a 36-character string. var document = new Document(null, $"doc-{Guid.NewGuid():N}", DocumentFormat.Json); db.Add(document); diff --git a/SW.Bitween.IntegrationTests/Tests/DeadLetterRequeueTests.cs b/SW.Bitween.IntegrationTests/Tests/DeadLetterRequeueTests.cs new file mode 100644 index 00000000..16e4a456 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/DeadLetterRequeueTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using RabbitMQ.Client; +using RabbitMQ.Client.Exceptions; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Resources.Ops; +using SW.Bus.RabbitMqExtensions; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Sending dead-lettered messages back to the queue they failed on, against a real broker: only +/// what was asked for, with a fresh retry count, and never into a queue that isn't there. +/// +[Collection("Bitween")] +public class DeadLetterRequeueTests(BitweenFixture fixture) +{ + private static int _seq; + + private async Task Broker() + { + await using var scope = fixture.CreateScope(); + var url = scope.ServiceProvider.GetRequiredService().GetConnectionString("RabbitMQ"); + return new ConnectionFactory { Uri = new Uri(url!) }.CreateConnection(); + } + + private DeadLetterQueues Service(params string[] known) => + new(DashboardStub.With(known), null!, fixture.CreateScope().ServiceProvider.GetRequiredService(), + NullLogger.Instance); + + /// A lane - main and bad queue - with messages dead-lettered. + private async Task Lane(int dead, bool withMain = true) + { + var main = $"test.deadletters.lane{Interlocked.Increment(ref _seq)}.{Guid.NewGuid():N}"; + using var connection = await Broker(); + using var channel = connection.CreateModel(); + if (withMain) channel.QueueDeclare(main, durable: false, exclusive: false, autoDelete: false); + channel.QueueDeclare($"{main}.bad", durable: false, exclusive: false, autoDelete: false); + for (var i = 1; i <= dead; i++) + { + var props = channel.CreateBasicProperties(); + props.CorrelationId = $"c{i}"; + props.Headers = new Dictionary + { + ["x-death"] = new List { new Dictionary { ["count"] = 5L } }, + ["exception0"] = "boom", + ["tenant"] = "acme", + }; + channel.BasicPublish("", $"{main}.bad", props, Encoding.UTF8.GetBytes($"{{\"n\":{i}}}")); + } + + return main; + } + + private async Task> Drain(string queue) + { + using var connection = await Broker(); + using var channel = connection.CreateModel(); + var got = new List(); + while (channel.BasicGet(queue, autoAck: true) is { } message) got.Add(message); + return got; + } + + [Fact] + public async Task The_oldest_go_back_with_a_fresh_retry_count_and_everything_else_intact() + { + var main = await Lane(3); + + Assert.Equal(2, Service().Requeue($"{main}.bad", 2)); + + var requeued = await Drain(main); + Assert.Equal(["{\"n\":1}", "{\"n\":2}"], requeued.Select(m => Encoding.UTF8.GetString(m.Body.ToArray()))); + var headers = requeued[0].BasicProperties.Headers; + Assert.False(headers.ContainsKey("x-death")); + Assert.False(headers.ContainsKey("exception0")); + Assert.Equal("acme", Encoding.UTF8.GetString((byte[])headers["tenant"])); + Assert.Equal("c1", requeued[0].BasicProperties.CorrelationId); + + Assert.Single(await Drain($"{main}.bad")); + } + + [Fact] + public async Task Nothing_moves_when_the_queue_they_failed_on_is_gone() + { + var main = await Lane(1, withMain: false); + + Assert.ThrowsAny(() => Service().Requeue($"{main}.bad", 10)); + + Assert.Single(await Drain($"{main}.bad")); + } + + [Fact] + public async Task Only_a_queue_the_bus_lists_as_dead_letters_is_touched() + { + var service = Service("v3.test.bitween.xchangeservice.xchangecreated.bad"); + + Assert.Equal("v3.test.bitween.xchangeservice.xchangecreated.bad", + await service.Resolve("V3.TEST.BITWEEN.XCHANGESERVICE.XCHANGECREATED.BAD")); + Assert.Null(await service.Resolve("some.other.queue")); + + await using var scope = fixture.CreateScope(); + var ctx = scope.Superuser(); + var handler = new RequeueDeadLetters(service, + scope.ServiceProvider.GetRequiredService(), ctx); + await Assert.ThrowsAsync(() => + handler.Handle(new RequeueDeadLettersModel { Queue = "some.other.queue" })); + } + + [Fact] + public async Task The_host_can_build_it() + { + await using var scope = fixture.CreateScope(); + Assert.NotNull(scope.ServiceProvider.GetRequiredService()); + } + + /// The bus's dashboard, listing just the dead-letter queues a test names. + public class DashboardStub : DispatchProxy + { + private string[] _queues = []; + + public static IBusDashboardDataService With(string[] queues) + { + var stub = Create(); + ((DashboardStub)(object)stub)._queues = queues; + return stub; + } + + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + if (targetMethod!.Name != nameof(IBusDashboardDataService.GetDeadLetterSummaryAsync)) + throw new NotSupportedException(targetMethod.Name); + var views = _queues.Select(q => + new DeadLetterSummaryView("XchangeService", "XchangeCreated", q, 1, null, null, null, default)).ToArray(); + var resultType = targetMethod.ReturnType.GetGenericArguments()[0]; + object result = resultType.IsArray ? views : views.ToList(); + return typeof(Task).GetMethod(nameof(Task.FromResult))!.MakeGenericMethod(resultType) + .Invoke(null, [result]); + } + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/DeduplicationTests.cs b/SW.Bitween.IntegrationTests/Tests/DeduplicationTests.cs index dd36b1d4..d837dc53 100644 --- a/SW.Bitween.IntegrationTests/Tests/DeduplicationTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DeduplicationTests.cs @@ -123,7 +123,7 @@ public async Task A_window_of_zero_turns_deduplication_off() } /// - /// Atomicity. A failed ingest must NOT leave the key behind — remembering a message that was + /// Atomicity. A failed ingest must NOT leave the key behind - remembering a message that was /// never persisted suppresses its redelivery for ever, which is silent data loss and the worse /// of the two failure modes. /// diff --git a/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs index 16491f91..c7d83283 100644 --- a/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs @@ -137,7 +137,7 @@ public async Task RunNow_executes_immediately_even_when_not_yet_due_and_removes_ var ctx = scope.Superuser(); var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, "Run Now Doc"); - // Scheduled an hour from now — RunNow must still execute it immediately. + // Scheduled an hour from now - RunNow must still execute it immediately. db.Set().Add(new DelayedRetry { Id = xchange.Id, On = DateTime.UtcNow.AddHours(1) }); await db.SaveChangesAsync(); diff --git a/SW.Bitween.IntegrationTests/Tests/DocumentSchemaValidationTests.cs b/SW.Bitween.IntegrationTests/Tests/DocumentSchemaValidationTests.cs new file mode 100644 index 00000000..64a3eb23 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/DocumentSchemaValidationTests.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using SW.Bitween.Controllers; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// An information type carrying a schema: kept across saves that don't mention it, enforced at +/// the gateway while the partner is still waiting, and on every exchange however it arrived. +/// What counts as a match is SW.Bitween.UnitTests' business (DocumentSchemaTests). +/// +[Collection("Bitween")] +public class DocumentSchemaValidationTests(BitweenFixture fixture) +{ + private const string Schema = """{ "type": "object", "required": ["orderId"] }"""; + private const string Responder = nameof(Adapters.NativeTestResponder); + + private static int _seq; + private static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; + + private async Task CreateType(string schema) + { + await using var scope = fixture.CreateScope(); + scope.Superuser(); + return (int)await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new DocumentCreate + { Name = Unique("Schema type"), DocumentFormat = DocumentFormat.Json, ValidationSchema = schema }); + } + + private async Task GetType(int id) + { + await using var scope = fixture.CreateScope(); + scope.Superuser(); + return (DocumentRow)await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(id); + } + + private async Task UpdateType(int id, Action change) + { + var current = await GetType(id); + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var model = new DocumentUpdate + { + Id = id, Name = current.Name, Code = current.Code, DocumentFormat = current.DocumentFormat, + PromotedProperties = current.PromotedProperties, + }; + change(model); + await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Handle(id, model); + } + + [Fact] + public async Task A_schema_is_kept_by_saves_that_leave_it_out_and_removed_by_an_empty_one() + { + var id = await CreateType(Schema); + Assert.Equal(Schema, (await GetType(id)).ValidationSchema); + + await UpdateType(id, m => m.Name += " renamed"); + Assert.Equal(Schema, (await GetType(id)).ValidationSchema); + + await UpdateType(id, m => m.ValidationSchema = ""); + Assert.Null((await GetType(id)).ValidationSchema); + } + + [Fact] + public async Task A_type_cannot_change_to_a_format_its_schema_does_not_fit() + { + var id = await CreateType(Schema); + + await Assert.ThrowsAsync(() => + UpdateType(id, m => m.DocumentFormat = DocumentFormat.Xml)); + } + + [Fact] + public async Task An_unusable_schema_is_refused_on_create() + { + await Assert.ThrowsAsync(() => CreateType("""{ "$ref": "http://169.254.169.254/" }""")); + } + + [Fact] + public async Task The_gateway_refuses_a_call_that_does_not_match_and_makes_no_exchange() + { + var docId = await CreateType(Schema); + var urlName = Unique("schema").ToLowerInvariant(); + var key = Guid.NewGuid().ToString("N"); + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner(Unique("Schema partner")); + partner.SetApiCredentials([new ApiCredential("main", key)]); + db.Add(partner); + var sub = new Subscription(Unique("Schema sub"), docId, SubscriptionType.GatewayApiCall) { Inactive = false }; + db.Add(sub); + await db.SaveChangesAsync(); + db.Add(new ApiGateway + { + Name = Unique("Schema gateway"), UrlName = urlName, + Partners = [new ApiGatewayPartner { PartnerId = partner.Id, SubscriptionId = sub.Id }], + }); + await db.SaveChangesAsync(); + } + + async Task Call(string body) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Revoke(); + scope.ServiceProvider.GetRequiredService().Set(new ClaimsPrincipal(new ClaimsIdentity()), + [new RequestValue("partnerkey", key, RequestValueType.HttpHeader)]); + var controller = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + controller.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + return await controller.Post($"{urlName}/async"); + } + + var refused = Assert.IsType(await Call("""{ "customer": "x" }""")); + Assert.Contains("orderId", JsonConvert.SerializeObject(refused.Value)); + Assert.IsType(await Call("""{ "orderId": "A1" }""")); + + await using (var scope = fixture.CreateScope()) + Assert.Equal(1, await scope.ServiceProvider.GetRequiredService() + .Set().CountAsync(x => x.DocumentId == docId)); + } + + [Fact] + public async Task An_exchange_whose_input_does_not_match_fails_with_what_is_wrong() + { + var docId = await CreateType(Schema); + int subId; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var sub = new Subscription(Unique("Schema delivery"), docId, SubscriptionType.GatewayApiCall) + { Inactive = false, HandlerId = Responder }; + db.Add(sub); + await db.SaveChangesAsync(); + subId = sub.Id; + } + + async Task Deliver(string body) + { + string xchangeId; + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + xchangeId = await scope.ServiceProvider.GetRequiredService() + .SubmitSubscriptionXchange(subId, new XchangeFile(body)); + } + + await using (var scope = fixture.CreateScope()) + await scope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + + await using (var scope = fixture.CreateScope()) + return await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(r => r.Id == xchangeId); + } + + var failed = await Deliver("""{ "customer": "x" }"""); + Assert.False(failed.Success); + Assert.Contains(nameof(DocumentSchemaException), failed.Exception); + Assert.Contains("orderId", failed.Exception); + + var passed = await Deliver("""{ "orderId": "A1" }"""); + Assert.True(passed.Success, passed.Exception); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ExchangeRetentionTests.cs b/SW.Bitween.IntegrationTests/Tests/ExchangeRetentionTests.cs index d9206b27..a187fc1c 100644 --- a/SW.Bitween.IntegrationTests/Tests/ExchangeRetentionTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ExchangeRetentionTests.cs @@ -26,7 +26,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// settings page shows before anyone changes either. /// /// -/// The collection shares one database, and the job removes every exchange older than its cutoff — +/// The collection shares one database, and the job removes every exchange older than its cutoff - /// so the exchanges here are backdated far past anything another test would create, and the job runs /// with a cutoff only they fall behind. /// @@ -366,7 +366,7 @@ await scope.ServiceProvider.GetRequiredService() var links = JsonConvert.DeserializeObject( await scope.ServiceProvider.GetRequiredService().GetFile(rollUp, XchangeFileType.Input)); - // Still a list of links, so handlers written for it are unaffected — just links Bitween + // Still a list of links, so handlers written for it are unaffected - just links Bitween // serves, since the storage URLs they replace don't open private files. var service = scope.ServiceProvider.GetRequiredService(); var fileLinks = scope.ServiceProvider.GetRequiredService(); @@ -428,7 +428,7 @@ public async Task Retrying_a_roll_up_made_before_the_upgrade_swaps_its_storage_l var service = scope.ServiceProvider.GetRequiredService(); var cloudFiles = scope.ServiceProvider.GetRequiredService(); var sourceKey = service.FileKey(source, XchangeFileType.Input); - // What a roll-up held before: the storage URL of each collected file — next to anything else. + // What a roll-up held before: the storage URL of each collected file - next to anything else. string[] before = [ cloudFiles.GetUrl(sourceKey), @@ -486,7 +486,7 @@ private sealed class FixedStorageAccess(IServiceProvider serviceProvider, Bitwee Task.FromResult(open); } - // ———————————————————————————————————————————————————————————————— helpers + // ---------------------------------------------------------------- helpers private sealed class ListeningOn : IServer { diff --git a/SW.Bitween.IntegrationTests/Tests/ExchangeSummaryTests.cs b/SW.Bitween.IntegrationTests/Tests/ExchangeSummaryTests.cs new file mode 100644 index 00000000..bcaecc87 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ExchangeSummaryTests.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.Bitween.Resources.Xchanges; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// The exchange list's newer filters (retries, scheduled retries, aggregated, error text) and the +/// summary over the list: per-status counts under every filter but the status itself, and a +/// histogram of when the matching exchanges started. +/// +/// +/// Every test scopes itself to an information type of its own with a DocumentId filter: the +/// collection shares one database, so a count over anything wider would count other tests' rows. +/// +[Collection("Bitween")] +public class ExchangeSummaryTests(BitweenFixture fixture) +{ + /// One exchange of each status on a fresh information type, plus a second failure. + private sealed record Seeded(int DocumentId, Subscription Subscription, Xchange Running, Xchange Succeeded, + Xchange BadResponse, Xchange Failed, Xchange FailedAgain); + + private static async Task OneOfEach(BitweenDbContext db, XchangeService xs, string name) + { + var doc = new Document(null, name, DocumentFormat.Json); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription(name, doc.Id) { Inactive = false }; + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var running = await xs.CreateXchange(sub, new XchangeFile("{}")); + var succeeded = await xs.CreateXchange(sub, new XchangeFile("{}")); + var badResponse = await xs.CreateXchange(sub, new XchangeFile("{}")); + var failed = await xs.CreateXchange(sub, new XchangeFile("{}")); + var failedAgain = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + db.Set().AddRange( + new XchangeResult(succeeded.Id, null, null), + new XchangeResult(badResponse.Id, null, null, new XchangeFile("{}", "response.json", true)), + new XchangeResult(failed.Id, null, null, exception: "Timeout Talking To ACME after 30s"), + new XchangeResult(failedAgain.Id, null, null, exception: "Mapping failed: no such field")); + await db.SaveChangesAsync(); + + return new Seeded(doc.Id, sub, running, succeeded, badResponse, failed, failedAgain); + } + + private static async Task SetStartedOn(BitweenDbContext db, string id, DateTime startedOn) => + await db.Set().Where(x => x.Id == id) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.StartedOn, startedOn)); + + private static async Task Summarise(AsyncServiceScope scope, params string[] filters) + { + var db = scope.ServiceProvider.GetRequiredService(); + return (XchangeSummary)await new Summary(db, scope.Superuser()).Handle(new SearchyRequest(filters)); + } + + private static async Task> Search(AsyncServiceScope scope, params string[] filters) + { + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var response = (SearchyResponse)await new Resources.Xchanges.Search(db, xs, scope.Superuser()) + .Handle(new SearchyRequest(filters) { PageSize = 50 }); + Assert.Equal(response.Result.Count(), response.TotalCount); + return response.Result.Select(r => r.Id).ToList(); + } + + /// What "select all matching" would act on for the same filter, as bulk retry and export resolve it. + private static async Task> Select(AsyncServiceScope scope, string filter) + { + var db = scope.ServiceProvider.GetRequiredService(); + return await new XchangeSelection(db).Resolve(new XchangeFilesExport { Filter = filter }, 100); + } + + // ─── The new filters ────────────────────────────────────────────────────── + + [Fact] + public async Task IsRetry_keeps_only_attempts_made_from_another_exchange() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary IsRetry Doc"); + await new Retry(db, scope.Superuser(), xs).Handle(seeded.Failed.Id, new XchangeRetry { Reset = false }); + await db.SaveChangesAsync(); + var retry = await db.Set().SingleAsync(x => x.RetryFor == seeded.Failed.Id); + + var doc = $"DocumentId:1:{seeded.DocumentId}"; + Assert.Equal([retry.Id], await Search(scope, doc, "IsRetry:1:true")); + Assert.Equal([retry.Id], await Select(scope, $"filter={doc}&filter=IsRetry:1:1")); + + // Off is no filter, as LatestOnly:1:false is. + Assert.Equal(6, (await Search(scope, doc, "IsRetry:1:false")).Count); + } + + [Fact] + public async Task HasScheduledRetry_keeps_only_exchanges_with_a_pending_delayed_retry() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Scheduled Doc"); + // A day out, so the retry job cannot pick it up and delete the row mid-test. + db.Set().Add(new DelayedRetry { Id = seeded.Failed.Id, On = DateTime.UtcNow.AddDays(1) }); + await db.SaveChangesAsync(); + + var doc = $"DocumentId:1:{seeded.DocumentId}"; + Assert.Equal([seeded.Failed.Id], await Search(scope, doc, "HasScheduledRetry:1:true")); + Assert.Equal([seeded.Failed.Id], await Select(scope, $"filter={doc}&filter=HasScheduledRetry:1:true")); + Assert.Equal(5, (await Search(scope, doc, "HasScheduledRetry:1:no")).Count); + } + + [Fact] + public async Task Aggregated_keeps_only_exchanges_rolled_up_into_an_aggregation() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Aggregated Doc"); + db.Set().Add(new XchangeAggregation(seeded.Succeeded.Id, seeded.Running.Id)); + await db.SaveChangesAsync(); + + var doc = $"DocumentId:1:{seeded.DocumentId}"; + Assert.Equal([seeded.Succeeded.Id], await Search(scope, doc, "Aggregated:1:true")); + Assert.Equal([seeded.Succeeded.Id], await Select(scope, $"filter={doc}&filter=Aggregated:1:true")); + } + + [Fact] + public async Task ErrorContains_matches_error_text_whatever_its_case() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Error Doc"); + + var doc = $"DocumentId:1:{seeded.DocumentId}"; + Assert.Equal([seeded.Failed.Id], await Search(scope, doc, "ErrorContains:4:talking to acme")); + Assert.Equal([seeded.Failed.Id], await Select(scope, $"filter={doc}&filter=ErrorContains:4:TIMEOUT")); + + // Exchanges with no error at all are not matched by any text, and both failures are by + // text they share. + Assert.Equal(new[] { seeded.Failed.Id, seeded.FailedAgain.Id }.OrderBy(i => i), + (await Search(scope, doc, "ErrorContains:4:A")).OrderBy(i => i)); + } + + // ─── The summary ────────────────────────────────────────────────────────── + + [Fact] + public async Task Summary_counts_every_status_and_ignores_a_status_filter() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Counts Doc"); + + // The failed tab is selected; the summary still answers for all four tabs. + var summary = await Summarise(scope, $"DocumentId:1:{seeded.DocumentId}", "StatusFilter:1:3"); + + Assert.Equal(1, summary.Counts.Processing); + Assert.Equal(1, summary.Counts.Success); + Assert.Equal(1, summary.Counts.BadResponse); + Assert.Equal(2, summary.Counts.Failed); + + // No date range, so nothing to divide into buckets. + Assert.Empty(summary.Buckets); + Assert.Equal(0, summary.BucketMinutes); + + // The other special filters still apply. + var narrowed = await Summarise(scope, $"DocumentId:1:{seeded.DocumentId}", "ErrorContains:4:mapping"); + Assert.Equal(0, narrowed.Counts.Processing + narrowed.Counts.Success + narrowed.Counts.BadResponse); + Assert.Equal(1, narrowed.Counts.Failed); + } + + [Fact] + public async Task Summary_buckets_a_range_by_status_zero_filled_and_aligned_to_the_clock() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Buckets Doc"); + var day = new DateTime(2025, 3, 4, 0, 0, 0, DateTimeKind.Utc); + await SetStartedOn(db, seeded.Failed.Id, day.AddHours(10).AddMinutes(8)); + await SetStartedOn(db, seeded.Succeeded.Id, day.AddHours(10).AddMinutes(9)); + await SetStartedOn(db, seeded.Running.Id, day.AddHours(11).AddMinutes(31)); + await SetStartedOn(db, seeded.BadResponse.Id, day.AddHours(11).AddMinutes(59)); + // Outside the range: in neither the counts nor the buckets. + await SetStartedOn(db, seeded.FailedAgain.Id, day.AddHours(13)); + + // 10:07 to 12:00 is 114 one-minute buckets; five minutes, from 10:05, is 24. + var summary = await Summarise(scope, $"DocumentId:1:{seeded.DocumentId}", + "StartedOn:6:2025-03-04T10:07:00Z", "StartedOn:8:2025-03-04T12:00:00Z"); + + Assert.Equal(5, summary.BucketMinutes); + Assert.Equal(24, summary.Buckets.Count); + Assert.Equal(day.AddHours(10).AddMinutes(5), summary.Buckets[0].Start); + Assert.Equal(day.AddHours(12), summary.Buckets[^1].Start); + Assert.All(summary.Buckets.Skip(1).Zip(summary.Buckets), + pair => Assert.Equal(TimeSpan.FromMinutes(5), pair.First.Start - pair.Second.Start)); + + var first = summary.Buckets[0]; + Assert.Equal((0, 1, 0, 1), (first.Processing, first.Success, first.BadResponse, first.Failed)); + Assert.Equal(1, summary.Buckets.Single(b => b.Start == day.AddHours(11).AddMinutes(30)).Processing); + Assert.Equal(1, summary.Buckets.Single(b => b.Start == day.AddHours(11).AddMinutes(55)).BadResponse); + Assert.Equal(4, summary.Buckets.Sum(b => b.Processing + b.Success + b.BadResponse + b.Failed)); + + Assert.Equal(1, summary.Counts.Failed); + } + + [Fact] + public async Task Summary_groups_by_hour_and_by_day_for_longer_ranges() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var seeded = await OneOfEach(db, xs, "Summary Long Range Doc"); + var day = new DateTime(2025, 5, 1, 0, 0, 0, DateTimeKind.Utc); + await SetStartedOn(db, seeded.Failed.Id, day.AddHours(5).AddMinutes(40)); + await SetStartedOn(db, seeded.FailedAgain.Id, day.AddDays(1).AddHours(23).AddMinutes(59)); + await SetStartedOn(db, seeded.Succeeded.Id, day.AddDays(2).AddHours(3)); + await SetStartedOn(db, seeded.Running.Id, day.AddDays(20)); + await SetStartedOn(db, seeded.BadResponse.Id, day.AddDays(29).AddHours(12)); + + var doc = $"DocumentId:1:{seeded.DocumentId}"; + + // Three days: 73 hours is too many, two-hour buckets are 37 - grouped by the hour. + var threeDays = await Summarise(scope, doc, "StartedOn:6:2025-05-01T00:00:00Z", "StartedOn:8:2025-05-04T00:00:00Z"); + Assert.Equal(120, threeDays.BucketMinutes); + Assert.Equal(37, threeDays.Buckets.Count); + Assert.Equal(1, threeDays.Buckets.Single(b => b.Start == day.AddHours(4)).Failed); + Assert.Equal(1, threeDays.Buckets.Single(b => b.Start == day.AddDays(1).AddHours(22)).Failed); + Assert.Equal(1, threeDays.Buckets.Single(b => b.Start == day.AddDays(2).AddHours(2)).Success); + Assert.Equal(3, threeDays.Buckets.Sum(b => b.Processing + b.Success + b.BadResponse + b.Failed)); + + // Five weeks: 71 half-days is too many, so a bucket a day - grouped by the day. + var month = await Summarise(scope, doc, "StartedOn:6:2025-05-01T00:00:00Z", "StartedOn:8:2025-06-05T00:00:00Z"); + Assert.Equal(1440, month.BucketMinutes); + Assert.Equal(36, month.Buckets.Count); + Assert.Equal(1, month.Buckets[0].Failed); + Assert.Equal(1, month.Buckets[1].Failed); + Assert.Equal(1, month.Buckets[2].Success); + Assert.Equal(1, month.Buckets[20].Processing); + Assert.Equal(1, month.Buckets[29].BadResponse); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ExternalBusGatewayTests.cs b/SW.Bitween.IntegrationTests/Tests/ExternalBusGatewayTests.cs index b8120510..7d8ec850 100644 --- a/SW.Bitween.IntegrationTests/Tests/ExternalBusGatewayTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ExternalBusGatewayTests.cs @@ -26,7 +26,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// broker becomes an Xchange in Bitween through a resident adapter. /// /// The external broker is a second container, deliberately. Reusing the internal one would let a -/// test pass while the message actually travelled Bitween's own bus — the precise confusion this +/// test pass while the message actually travelled Bitween's own bus - the precise confusion this /// feature has to avoid. /// [Collection("Bitween")] @@ -52,7 +52,7 @@ public async Task A_message_on_an_external_broker_becomes_an_Xchange() /// /// The ordering the whole design turns on. Until Bitween has persisted, the message must still - /// be the broker's — so a Bitween failure stops draining the customer's queue rather than + /// be the broker's - so a Bitween failure stops draining the customer's queue rather than /// losing their messages. /// [Fact] @@ -79,7 +79,7 @@ await WaitAsync(() => Depth(queue) == 0, TimeSpan.FromSeconds(30), /// /// This replaces a test that published malformed content and expected a nack. That premise was /// wrong: Bitween persists first and validates afterwards, so bad content becomes an Xchange - /// carrying a bad result — a pipeline outcome, not an ingest failure. Exercising the rejection + /// carrying a bad result - a pipeline outcome, not an ingest failure. Exercising the rejection /// path means making the SINK fail, which is what an unattributable event does. /// /// That the adapter then nacks and the broker redelivers is proven against a real broker in @@ -105,7 +105,7 @@ public async Task The_sink_rejects_an_event_it_cannot_attribute_to_a_data_source /// /// The opposite case, and it must NOT reject. An endpoint no gateway claims is a - /// misconfiguration, not a Bitween failure — rejecting would requeue it for ever and the + /// misconfiguration, not a Bitween failure - rejecting would requeue it for ever and the /// customer's queue would never drain. /// [Fact] @@ -132,7 +132,7 @@ public async Task Each_gateway_receives_only_its_own_endpoint() var invoices = Unique("invoices"); var shipments = Unique("shipments"); - // Both, because the adapter consumes what the DATA SOURCE lists — a gateway naming an + // Both, because the adapter consumes what the DATA SOURCE lists - a gateway naming an // endpoint nobody is consuming would simply never see a message. var dataSourceId = await CreateDataSourceAsync($"{invoices},{shipments}", withGateway: false); var invoiceDoc = await AddGatewayAsync(dataSourceId, invoices); @@ -155,7 +155,7 @@ public async Task Each_gateway_receives_only_its_own_endpoint() /// /// A gateway with no endpoint is the catch-all: it claims whatever no other gateway does. The /// exact match has to win, or a data source that gains a catch-all silently starts routing - /// every specific endpoint's messages to the wrong Document — running the wrong subscriptions, + /// every specific endpoint's messages to the wrong Document - running the wrong subscriptions, /// against the wrong mapping, with nothing in the audit trail saying so. /// /// Driven through the sink rather than the broker: this is a routing decision, and the queue @@ -191,7 +191,7 @@ public async Task An_exact_endpoint_match_beats_the_catch_all_gateway() } /// - /// And the catch-all still catches what nothing else claims — otherwise the fix above would + /// And the catch-all still catches what nothing else claims - otherwise the fix above would /// just be a way of disabling it. /// [Fact] @@ -244,7 +244,7 @@ public async Task A_data_source_still_feeding_a_gateway_cannot_be_deleted() db.Remove(await db.Set().FirstAsync(d => d.Id == dataSourceId)); await Assert.ThrowsAnyAsync(() => db.SaveChangesAsync()); - // And the gateway is untouched — still external, still on its data source. + // And the gateway is untouched - still external, still on its data source. await using var check = fixture.App.Services.CreateAsyncScope(); var fresh = check.ServiceProvider.GetRequiredService(); var gateway = await fresh.Set().AsNoTracking() @@ -283,8 +283,8 @@ await WaitAsync(() => Depth(target) >= 1, TimeSpan.FromSeconds(15), /// /// A DELIVERY publishes, which is the half that was missing. Publish had existed since this - /// adapter did and nothing in the pipeline ever called it — the handler contract is Handle, - /// and this class had none — so an integration could drain a customer's queue and had no way + /// adapter did and nothing in the pipeline ever called it - the handler contract is Handle, + /// and this class had none - so an integration could drain a customer's queue and had no way /// to answer on one. /// /// Driven the way the pipeline drives it: Handle, with the endpoint arriving as a @@ -339,11 +339,11 @@ public async Task A_delivery_with_no_endpoint_says_so() /// /// A broker data source is EXCLUSIVE: one node holds the connection so the customer's queue is /// drained once. A subscription's delivery, though, runs on whichever node picked the message - /// up — so a publish would fail on every node but the owner, which is to say almost always. + /// up - so a publish would fail on every node but the owner, which is to say almost always. /// /// Exclusivity is about CONSUMING, not about connecting. When the owned instance is not here, - /// the runtime opens a send-only connection of its own — Consume=false, no endpoints, its own - /// pool key — and publishes through that. Nothing is consumed twice, and a delivery works + /// the runtime opens a send-only connection of its own - Consume=false, no endpoints, its own + /// pool key - and publishes through that. Nothing is consumed twice, and a delivery works /// wherever it lands. /// /// Here nothing is running under the data source's instance key at all, which is exactly what @@ -499,7 +499,7 @@ private async Task AddGatewayAsync(int dataSourceId, string endpoint) // The infolink cache is a singleton holding a ten-minute snapshot. Without revoking it, // XchangeService resolves this brand-new Document to null and builds an Xchange that no - // DocumentId query can find — which acks the message and silently drops it. + // DocumentId query can find - which acks the message and silently drops it. scope.ServiceProvider.GetRequiredService().Revoke(); return document.Id; @@ -517,7 +517,7 @@ private async Task StartAsync(int dataSourceId) var instance = await host.StartExclusiveAsync(new AdapterSpec { AdapterId = dataSource.AdapterId, - // The instance key IS the data source id — that is how the sink resolves the gateway. + // The instance key IS the data source id - that is how the sink resolves the gateway. InstanceKey = dataSourceId.ToString(), StartupValues = new Dictionary(dataSource.Properties) }); diff --git a/SW.Bitween.IntegrationTests/Tests/GatewayRoutingTests.cs b/SW.Bitween.IntegrationTests/Tests/GatewayRoutingTests.cs index 102a1560..53042968 100644 --- a/SW.Bitween.IntegrationTests/Tests/GatewayRoutingTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/GatewayRoutingTests.cs @@ -17,8 +17,8 @@ namespace SW.Bitween.IntegrationTests.Tests; /// Which integrations run when a document arrives on the bus. /// /// -/// is the dispatcher: one message lands, and it decides — from the -/// payload alone — which integrations are handed it. Everything downstream is a consequence of +/// is the dispatcher: one message lands, and it decides - from the +/// payload alone - which integrations are handed it. Everything downstream is a consequence of /// what it returns, and until now nothing exercised it. Both directions matter equally here: an /// integration that stops being selected goes quiet with no error anywhere, and one that is /// selected when it shouldn't be runs real traffic through the wrong pipeline. @@ -76,7 +76,7 @@ public async Task A_route_with_no_filter_runs_its_integration_for_every_message( db.Set().Add(gateway); await db.SaveChangesAsync(); - // No MatchExpression at all — the "send me everything on this type" route. + // No MatchExpression at all - the "send me everything on this type" route. db.Set().Add(new BusGatewayRoute { BusGatewayId = gateway.Id, @@ -130,7 +130,7 @@ public async Task A_route_filter_selects_only_the_messages_it_names() var result = await Dispatch(docId, "{\"country\":\"JO\",\"channel\":\"web\"}"); - // Selecting is the entire job — a filter that lets everything through is the same bug as + // Selecting is the entire job - a filter that lets everything through is the same bug as // one that lets nothing through, and only checking both sides catches it. Assert.Contains(result.GatewayHits, h => h.SubscriptionId == jordan); Assert.DoesNotContain(result.GatewayHits, h => h.SubscriptionId == emirates); @@ -171,7 +171,7 @@ public async Task A_route_carries_its_partner_to_the_integration_it_runs() // A bus gateway integration has no partner of its own, so this is the only place the // partner can come from. Lose it here and every {{partner.…}} in its adapters stays a - // literal token — the request goes out to a URL with the placeholder still in it. + // literal token - the request goes out to a URL with the placeholder still in it. var hit = Assert.Single(result.GatewayHits, h => h.SubscriptionId == integrationId); Assert.Equal(partnerId, hit.PartnerId); } @@ -253,7 +253,7 @@ public async Task An_integration_with_its_own_entry_point_is_not_run_by_a_messag [SubscriptionType.Response] = new("Entry response", docId, SubscriptionType.Response) { Inactive = false }, // The control. Without one, every assertion below would also pass if the - // subscriptions simply never reached the dispatcher — an empty result looks + // subscriptions simply never reached the dispatcher - an empty result looks // identical to a correctly filtered one. [SubscriptionType.Internal] = new("Entry control", docId, SubscriptionType.Internal, partner.Id) { Inactive = false }, @@ -308,7 +308,7 @@ public async Task An_internal_integration_still_runs_when_its_document_arrives() var result = await Dispatch(docId, "{\"country\":\"JO\",\"channel\":\"web\"}"); // The counterweight to the test above: reacting to a document of its type arriving is the - // whole definition of an Internal integration — it has no other trigger, so a guard that + // whole definition of an Internal integration - it has no other trigger, so a guard that // over-reaches silences it with nothing to show for it. Assert.Contains(matching, result.Hits); Assert.DoesNotContain(filteredOut, result.Hits); diff --git a/SW.Bitween.IntegrationTests/Tests/GrantLimitTests.cs b/SW.Bitween.IntegrationTests/Tests/GrantLimitTests.cs new file mode 100644 index 00000000..f3d16119 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/GrantLimitTests.cs @@ -0,0 +1,136 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Managing members and roles can be delegated through a custom role. Holding users.edit or +/// roles.edit must not be a way to Administrator: a caller grants only what they hold, and leaves +/// alone anyone who holds more than they do. +/// +[Collection("Bitween")] +public class GrantLimitTests(BitweenFixture fixture) +{ + private static readonly string[] HelpdeskPermissions = + [Permissions.Users.View, Permissions.Users.Edit, Permissions.Users.Create]; + + private static readonly string[] RoleEditorPermissions = + [Permissions.Roles.View, Permissions.Roles.Create, Permissions.Roles.Edit]; + + [Fact] + public async Task A_delegated_user_manager_cannot_make_themselves_administrator() + { + await using var scope = fixture.CreateScope(); + var (helpdesk, _) = await SignInWithCustomRole(scope, "self-admin", HelpdeskPermissions); + + var setRoles = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + setRoles.Handle(helpdesk, new SetAccountRolesModel { RoleIds = [Role.AdministratorId] })); + } + + [Fact] + public async Task A_delegated_user_manager_cannot_set_an_administrators_password() + { + await using var scope = fixture.CreateScope(); + var admin = await CreateAccount(scope, Unique("admin-target"), Role.AdministratorId); + await SignInWithCustomRole(scope, "reset-admin", HelpdeskPermissions); + + var setPassword = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + setPassword.Handle(admin, new SetAccountPasswordModel { Password = "a-new-password-1" })); + } + + [Fact] + public async Task A_delegated_user_manager_can_grant_what_they_hold() + { + await using var scope = fixture.CreateScope(); + var (_, roleId) = await SignInWithCustomRole(scope, "grant-own", HelpdeskPermissions); + var colleague = await CreateAccount(scope, Unique("colleague")); + + var setRoles = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + await setRoles.Handle(colleague, new SetAccountRolesModel { RoleIds = [roleId] }); + + var db = scope.ServiceProvider.GetRequiredService(); + Assert.True(db.Set().Any(l => l.AccountId == colleague && l.RoleId == roleId)); + } + + [Fact] + public async Task A_role_editor_cannot_add_a_permission_they_do_not_hold() + { + await using var scope = fixture.CreateScope(); + var (_, roleId) = await SignInWithCustomRole(scope, "role-edit", RoleEditorPermissions); + + var update = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + update.Handle(roleId, new RoleUpdate + { + Name = Unique("escalated"), + Permissions = [.. RoleEditorPermissions, Permissions.Users.Edit], + })); + } + + [Fact] + public async Task A_role_editor_cannot_create_a_role_beyond_their_own() + { + await using var scope = fixture.CreateScope(); + await SignInWithCustomRole(scope, "role-create", RoleEditorPermissions); + + var create = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + create.Handle(new RoleCreate + { + Name = Unique("everything"), + Permissions = PermissionCatalog.AllKeys.ToList(), + })); + } + + [Fact] + public async Task An_administrator_is_not_limited() + { + await using var scope = fixture.CreateScope(); + var admin = await CreateAccount(scope, Unique("admin-caller"), Role.AdministratorId); + var target = await CreateAccount(scope, Unique("admin-grantee")); + scope.As(admin); + + var setRoles = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + await setRoles.Handle(target, new SetAccountRolesModel { RoleIds = [Role.AdministratorId] }); + } + + private static async Task<(int AccountId, int RoleId)> SignInWithCustomRole( + AsyncServiceScope scope, string label, string[] permissions) + { + var db = scope.ServiceProvider.GetRequiredService(); + var role = new Role(Unique(label), "Delegated for a test", permissions.ToList()); + db.Set().Add(role); + await db.SaveChangesAsync(); + + var account = await CreateAccount(scope, Unique(label), role.Id); + scope.As(account); + return (account, role.Id); + } + + private static async Task CreateAccount(AsyncServiceScope scope, string label, params int[] roleIds) + { + var db = scope.ServiceProvider.GetRequiredService(); + var account = new Account("Test User", $"{label}@test.local", "irrelevant-hash", AccountRole.Member); + db.Set().Add(account); + await db.SaveChangesAsync(); + + foreach (var roleId in roleIds) + db.Set().Add(new AccountRoleLink(account.Id, roleId)); + await db.SaveChangesAsync(); + return account.Id; + } + + private static string Unique(string prefix) => $"{prefix}-{System.Guid.NewGuid():N}"[..24]; +} diff --git a/SW.Bitween.IntegrationTests/Tests/IdempotencyKeyTests.cs b/SW.Bitween.IntegrationTests/Tests/IdempotencyKeyTests.cs new file mode 100644 index 00000000..f34e5a86 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/IdempotencyKeyTests.cs @@ -0,0 +1,208 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using SW.Bitween.Controllers; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// A partner whose call timed out can't tell whether it arrived. Sending it again with the same +/// Idempotency-Key must return the exchange the first call made, not run the integration twice. +/// +[Collection("Bitween")] +public class IdempotencyKeyTests(BitweenFixture fixture) +{ + private static int _seq; + private static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; + + /// A gateway with two partners attached, each with a key of its own. + private async Task<(string urlName, string key, string otherKey)> Gateway() + { + var urlName = Unique("idem").ToLowerInvariant(); + var key = Guid.NewGuid().ToString("N"); + var otherKey = Guid.NewGuid().ToString("N"); + + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner(Unique("Idem partner")); + partner.SetApiCredentials([new ApiCredential("main", key)]); + var other = new Partner(Unique("Idem other partner")); + other.SetApiCredentials([new ApiCredential("main", otherKey)]); + var doc = new Document(null, Unique("Idem doc"), DocumentFormat.Json); + db.AddRange(partner, other, doc); + await db.SaveChangesAsync(); + + var subscription = new Subscription(Unique("Idem sub"), doc.Id, SubscriptionType.GatewayApiCall) + { Inactive = false }; + db.Add(subscription); + await db.SaveChangesAsync(); + + db.Add(new ApiGateway + { + Name = Unique("Idem gateway"), + UrlName = urlName, + Partners = + [ + new ApiGatewayPartner { PartnerId = partner.Id, SubscriptionId = subscription.Id }, + new ApiGatewayPartner { PartnerId = other.Id, SubscriptionId = subscription.Id }, + ], + }); + await db.SaveChangesAsync(); + return (urlName, key, otherKey); + } + + private async Task<(IActionResult result, IHeaderDictionary headers)> Call(string urlName, string partnerKey, + string idempotencyKey) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Revoke(); + scope.ServiceProvider.GetRequiredService().Set( + new ClaimsPrincipal(new ClaimsIdentity()), + [new RequestValue("partnerkey", partnerKey, RequestValueType.HttpHeader)]); + + var controller = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + controller.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{}")); + if (idempotencyKey is not null) + controller.HttpContext.Request.Headers[IdempotencyGuard.Header] = idempotencyKey; + var result = await controller.Post($"{urlName}/async"); + return (result, controller.HttpContext.Response.Headers); + } + + private static string XchangeId(IActionResult result) => (string)Assert.IsType(result).Location; + + private async Task XchangeCount(params string[] ids) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService() + .Set().CountAsync(x => ids.Contains(x.Id)); + } + + [Fact] + public async Task The_same_key_sent_again_returns_the_first_exchange_and_makes_no_second() + { + var (urlName, key, _) = await Gateway(); + var idem = Guid.NewGuid().ToString(); + + var (first, firstHeaders) = await Call(urlName, key, idem); + var (second, secondHeaders) = await Call(urlName, key, idem); + + Assert.Equal(XchangeId(first), XchangeId(second)); + Assert.False(firstHeaders.ContainsKey("Idempotent-Replay")); + Assert.Equal("true", secondHeaders["Idempotent-Replay"]); + Assert.Equal(1, await XchangeCount(XchangeId(first))); + } + + [Fact] + public async Task Calls_without_a_key_or_with_different_keys_each_make_their_own_exchange() + { + var (urlName, key, _) = await Gateway(); + + var ids = new[] + { + XchangeId((await Call(urlName, key, null)).result), + XchangeId((await Call(urlName, key, null)).result), + XchangeId((await Call(urlName, key, "a")).result), + XchangeId((await Call(urlName, key, "b")).result), + }; + + Assert.Equal(4, ids.Distinct().Count()); + } + + [Fact] + public async Task Two_partners_using_the_same_key_do_not_get_each_others_exchange() + { + var (urlName, key, otherKey) = await Gateway(); + + var mine = XchangeId((await Call(urlName, key, "order-1")).result); + var theirs = XchangeId((await Call(urlName, otherKey, "order-1")).result); + + Assert.NotEqual(mine, theirs); + } + + [Fact] + public async Task A_key_past_its_window_makes_a_new_exchange() + { + var (urlName, key, _) = await Gateway(); + var idem = Guid.NewGuid().ToString(); + var first = XchangeId((await Call(urlName, key, idem)).result); + + await using (var scope = fixture.CreateScope()) + { + await scope.ServiceProvider.GetRequiredService().Set() + .Where(k => k.Key == idem) + .ExecuteUpdateAsync(s => s.SetProperty(k => k.CreatedOn, + DateTime.UtcNow - IdempotencyGuard.Window - TimeSpan.FromMinutes(1))); + } + + var second = XchangeId((await Call(urlName, key, idem)).result); + var third = XchangeId((await Call(urlName, key, idem)).result); + + Assert.NotEqual(first, second); + Assert.Equal(second, third); + } + + [Fact] + public async Task A_key_too_long_to_store_is_refused() + { + var (urlName, key, _) = await Gateway(); + + var (result, _) = await Call(urlName, key, new string('k', IdempotencyGuard.MaxKeyLength + 1)); + + Assert.IsType(result); + } + + [Fact] + public async Task The_older_partner_endpoint_honours_the_key_too() + { + var key = Guid.NewGuid().ToString("N"); + int documentId; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner(Unique("Legacy idem partner")); + partner.SetApiCredentials([new ApiCredential("main", key)]); + var doc = new Document(null, Unique("Legacy idem doc"), DocumentFormat.Json); + db.AddRange(partner, doc); + await db.SaveChangesAsync(); + db.Add(new Subscription(Unique("Legacy idem sub"), doc.Id, SubscriptionType.ApiCall, partner.Id) + { Inactive = false }); + await db.SaveChangesAsync(); + documentId = doc.Id; + } + + async Task Post(string idempotencyKey) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Revoke(); + scope.ServiceProvider.GetRequiredService().Set( + new ClaimsPrincipal(new ClaimsIdentity()), + [ + new RequestValue("partnerkey", key, RequestValueType.HttpHeader), + new RequestValue(IdempotencyGuard.Header, idempotencyKey, RequestValueType.HttpHeader), + ]); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + var result = (CqApiResult)await handler.Handle(documentId.ToString(), JObject.Parse("{}")); + return (string)result.Result; + } + + var first = await Post("legacy-1"); + Assert.Equal(first, await Post("legacy-1")); + Assert.NotEqual(first, await Post("legacy-2")); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/InformationTypeTests.cs b/SW.Bitween.IntegrationTests/Tests/InformationTypeTests.cs index 36ab41b5..41f984a7 100644 --- a/SW.Bitween.IntegrationTests/Tests/InformationTypeTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/InformationTypeTests.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// Information types — the kinds of document that flow through Bitween, and the shape every +/// Information types - the kinds of document that flow through Bitween, and the shape every /// integration is configured against. /// /// @@ -21,7 +21,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// a machine. Two types called "Invoice" and "invoice" are indistinguishable in every list that /// shows them; two publishing as "OrderPlaced" and "orderplaced" are one message on the wire, /// because the routing key is lower-cased at both ends. Neither produces an error at the moment it -/// is created — the damage shows up later as messages arriving somewhere nobody meant them to. +/// is created - the damage shows up later as messages arriving somewhere nobody meant them to. /// [Collection("Bitween")] public class InformationTypeTests(BitweenFixture fixture) @@ -111,7 +111,7 @@ public async Task Renaming_a_type_actually_persists() await Update(id, new DocumentUpdate { Name = newName, Code = newCode }); - // Name and Code have private setters, and the bulk property copy only writes public ones — + // Name and Code have private setters, and the bulk property copy only writes public ones - // so the rename used to be accepted, reported as saved, and silently discarded. The handler // now sets both explicitly; this is here so that cannot come back. var stored = await Stored(id); @@ -142,7 +142,7 @@ public async Task Sending_no_promoted_properties_clears_them_rather_than_failing }); Assert.Single((await Stored(id)).PromotedProperties); - // An absent list means none. Left implicit this threw ArgumentNullException — a 500 for a + // An absent list means none. Left implicit this threw ArgumentNullException - a 500 for a // request whose meaning the API had simply never decided. await Update(id, new DocumentUpdate { Name = Unique("Now has none") }); @@ -169,7 +169,7 @@ public async Task A_usable_json_path_is_accepted(string path) public async Task A_promoted_property_that_could_never_match_is_refused() { // These are read against every payload that arrives. A path that cannot resolve does not - // fail loudly — the property is simply blank on every exchange, and on every screen. + // fail loudly - the property is simply blank on every exchange, and on every screen. var ex = await Assert.ThrowsAsync(() => Create(new DocumentCreate { Name = Unique("Bad path"), diff --git a/SW.Bitween.IntegrationTests/Tests/LeaderElectionTests.cs b/SW.Bitween.IntegrationTests/Tests/LeaderElectionTests.cs index 185ff63c..90862dfa 100644 --- a/SW.Bitween.IntegrationTests/Tests/LeaderElectionTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/LeaderElectionTests.cs @@ -18,7 +18,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// A broker connection is exclusive: two nodes consuming one queue is duplicate processing, which /// is the failure the whole external-bus design exists to prevent. So these tests are about one -/// question — can two nodes ever believe they own the same resource at the same time? +/// question - can two nodes ever believe they own the same resource at the same time? /// /// Each ILeaderElection instance here stands in for a NODE. They share the database and the /// broker, which is exactly the situation a real cluster is in. @@ -43,7 +43,7 @@ public async Task Only_one_node_can_hold_a_resource() /// /// The race, rather than the sequence. Six nodes reaching for one resource at once must - /// produce exactly one winner — a lock that only works when contention is polite is not a lock. + /// produce exactly one winner - a lock that only works when contention is polite is not a lock. /// [Fact] public async Task Concurrent_nodes_produce_exactly_one_owner() @@ -161,7 +161,7 @@ public async Task A_lease_records_which_node_holds_it() private static string Unique(string prefix) => $"{prefix}.{Guid.NewGuid():N}"[..24]; /// - /// A node. Each gets its own election instance, and therefore its own broker connection — + /// A node. Each gets its own election instance, and therefore its own broker connection - /// which is what makes the exclusive-queue lock meaningful between them. /// private RabbitMqLeaderElection Node() => new( diff --git a/SW.Bitween.IntegrationTests/Tests/LoginTests.cs b/SW.Bitween.IntegrationTests/Tests/LoginTests.cs index 851ed493..8b8d6b0d 100644 --- a/SW.Bitween.IntegrationTests/Tests/LoginTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/LoginTests.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// The account login handler — the one door into the system, and the only place a password is +/// The account login handler - the one door into the system, and the only place a password is /// checked. /// /// @@ -31,14 +31,15 @@ public class LoginTests(BitweenFixture fixture) /// /// One attempt in its own scope, because that is what a real request is. Sharing a scope across /// attempts would share a DbContext, and the lockout counter is written with a database-side - /// UPDATE that the change tracker never sees — so a second attempt would read a stale account + /// UPDATE that the change tracker never sees - so a second attempt would read a stale account /// and the lockout would look broken when it isn't. /// - private async Task Login(string email, string password) + private async Task Login(string email, string password, string address = "203.0.113.1") { await using var scope = fixture.CreateScope(); var accessor = scope.ServiceProvider.GetRequiredService(); accessor.HttpContext = new DefaultHttpContext(); + accessor.HttpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Parse(address); var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); return await handler.Handle(new UserLogin { Username = email, Password = password }); } @@ -122,6 +123,20 @@ public async Task A_disabled_account_cannot_sign_in() Assert.Contains("disabled", ex.Message, StringComparison.OrdinalIgnoreCase); } + /// + /// Only someone who knows the password learns the account is disabled; anyone else gets the + /// answer an unknown address gets, so the message can't be used to list who has an account. + /// + [Fact] + public async Task A_wrong_password_on_a_disabled_account_gets_the_ordinary_refusal() + { + await CreateAccount("disabled-guess@test.local", disabled: true); + + var ex = await Assert.ThrowsAsync(() => Login("disabled-guess@test.local", "wrong")); + + Assert.Equal("Invalid username or password.", ex.Message); + } + [Fact] public async Task An_account_that_has_no_password_cannot_be_signed_into() { @@ -156,9 +171,37 @@ public async Task Repeated_wrong_passwords_lock_the_account() // The point of the lockout: even the real password stops working while it holds. var ex = await Assert.ThrowsAsync(() => Login("lockout@test.local", GoodPassword)); Assert.Contains("locked", ex.Message, StringComparison.OrdinalIgnoreCase); + } - var stored = await Reload(account.Id); - Assert.NotNull(stored.LockoutEnd); + /// + /// Five wrong guesses used to lock the account for everyone, so anyone who knew an address + /// could keep its owner out indefinitely. Now they lock out the address that made them. + /// + [Fact] + public async Task Guessing_from_one_address_does_not_lock_out_the_owner_elsewhere() + { + await CreateAccount("targeted@test.local"); + + for (var attempt = 0; attempt < 5; attempt++) + await Assert.ThrowsAsync(() => Login("targeted@test.local", "wrong", "198.51.100.7")); + + await Assert.ThrowsAsync(() => Login("targeted@test.local", GoodPassword, "198.51.100.7")); + Assert.NotNull(await Login("targeted@test.local", GoodPassword, "203.0.113.50")); + } + + /// The account-wide backstop, for guessing spread over many addresses. + [Fact] + public async Task Guessing_spread_over_many_addresses_still_locks_the_account() + { + var account = await CreateAccount("spread@test.local"); + + for (var attempt = 0; attempt < 20; attempt++) + await Assert.ThrowsAsync(() => + Login("spread@test.local", "wrong", $"198.51.100.{attempt % 4 + 10}")); + + var ex = await Assert.ThrowsAsync(() => Login("spread@test.local", GoodPassword, "203.0.113.99")); + Assert.Contains("locked", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.NotNull((await Reload(account.Id)).LockoutEnd); } [Fact] @@ -178,4 +221,157 @@ public async Task A_successful_login_clears_earlier_failures() var stored = await Reload(account.Id); Assert.Equal(0, stored.FailedLoginCount); } + + private async Task Refresh(string refreshToken) + { + await using var scope = fixture.CreateScope(); + var accessor = scope.ServiceProvider.GetRequiredService(); + accessor.HttpContext = new DefaultHttpContext(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + return await handler.Handle(new UserLogin { RefreshToken = refreshToken }); + } + + private async Task IssueRefreshToken(int accountId, TimeSpan age) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var token = new RefreshToken(accountId, LoginMethod.EmailAndPassword); + db.Add(token); + await db.SaveChangesAsync(); + var createdOn = DateTime.UtcNow - age; + await db.Set().Where(t => t.Id == token.Id) + .ExecuteUpdateAsync(u => u.SetProperty(t => t.CreatedOn, createdOn)); + return token.Id; + } + + [Fact] + public async Task A_recent_refresh_token_signs_in() + { + var account = await CreateAccount("refresh-fresh@test.local"); + var token = await IssueRefreshToken(account.Id, TimeSpan.FromDays(1)); + + Assert.NotNull(await Refresh(token)); + } + + /// + /// The cookie expires after 30 days; the stored token has to as well, or one copied out of a + /// browser, or sent in the body by a legacy client, works forever. + /// + [Fact] + public async Task A_refresh_token_idle_for_more_than_30_days_is_refused() + { + var account = await CreateAccount("refresh-stale@test.local"); + var token = await IssueRefreshToken(account.Id, TimeSpan.FromDays(31)); + + await Assert.ThrowsAsync(() => Refresh(token)); + await Assert.ThrowsAsync(() => Refresh(token)); + } + + /// + /// Disabling refused new sign-ins but not the token already issued, which kept every permission + /// until it expired. + /// + [Fact] + public async Task A_disabled_account_is_granted_nothing_on_a_token_it_already_holds() + { + var account = await CreateAccount("disabled-token@test.local"); + await using (var setup = fixture.CreateScope()) + { + var db = setup.ServiceProvider.GetRequiredService(); + db.Set().Add(new AccountRoleLink(account.Id, Role.AdministratorId)); + await db.SaveChangesAsync(); + await db.Set().Where(a => a.Id == account.Id) + .ExecuteUpdateAsync(u => u.SetProperty(a => a.Disabled, true)); + } + + await using var scope = fixture.CreateScope(); + var ctx = scope.As(account.Id); + Assert.Empty(await ctx.GetPermissions(scope.ServiceProvider.GetRequiredService())); + } + + private async Task LoginWithCookie(string email, string password, string refreshCookie) + { + await using var scope = fixture.CreateScope(); + var accessor = scope.ServiceProvider.GetRequiredService(); + accessor.HttpContext = new DefaultHttpContext(); + accessor.HttpContext.Request.Headers.Cookie = $"refresh_token={refreshCookie}"; + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + return await handler.Handle(new UserLogin { Username = email, Password = password }); + } + + private static string AccountIdIn(object loginResult) + { + var jwt = (string)loginResult.GetType().GetProperty("Jwt")!.GetValue(loginResult)!; + var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().ReadJwtToken(jwt); + return token.Claims.First(c => c.Type is "nameid" or System.Security.Claims.ClaimTypes.NameIdentifier).Value; + } + + /// + /// A sign-out whose request never completes leaves the refresh cookie in the browser. Signing in + /// as someone else then has to sign in as them - it used to prefer the cookie and hand back the + /// previous person's session. + /// + [Fact] + public async Task Signing_in_with_credentials_ignores_a_leftover_refresh_cookie() + { + var previous = await CreateAccount("leftover-previous@test.local"); + var next = await CreateAccount("leftover-next@test.local"); + var leftover = await IssueRefreshToken(previous.Id, TimeSpan.FromMinutes(1)); + + var result = await LoginWithCookie("leftover-next@test.local", GoodPassword, leftover); + + Assert.Equal(next.Id.ToString(), AccountIdIn(result)); + + // And the previous session's token does not survive as a second way in. + await using var scope = fixture.CreateScope(); + Assert.False(await scope.ServiceProvider.GetRequiredService() + .Set().AnyAsync(t => t.Id == leftover)); + } + + /// + /// Two requests carrying the same refresh token - a refresh racing a sign-out, or a page load + /// racing the one before it - used to both load it and both delete it, and the loser failed with + /// a 500. Within the grace after a token is replaced, a repeat is served, not refused, so a + /// browser whose copy of the new token was lost is not signed out. + /// + [Fact] + public async Task A_refresh_token_used_twice_at_once_does_not_fail() + { + var account = await CreateAccount("refresh-race@test.local"); + var token = await IssueRefreshToken(account.Id, TimeSpan.FromMinutes(1)); + + var attempts = await Task.WhenAll(Enumerable.Range(0, 4).Select(async _ => + { + try + { + await Refresh(token); + return "signed-in"; + } + catch (SWException) + { + return "refused"; + } + })); + + Assert.All(attempts, a => Assert.Equal("signed-in", a)); + } + + /// Past the grace, a replaced token is spent for good. + [Fact] + public async Task A_replaced_refresh_token_is_refused_once_its_grace_has_passed() + { + var account = await CreateAccount("refresh-replaced@test.local"); + var token = await IssueRefreshToken(account.Id, TimeSpan.FromMinutes(1)); + await Refresh(token); + + await using (var scope = fixture.CreateScope()) + { + var longAgo = DateTime.UtcNow.AddMinutes(-5); + await scope.ServiceProvider.GetRequiredService().Set() + .Where(t => t.Id == token) + .ExecuteUpdateAsync(u => u.SetProperty(t => t.SupersededOn, longAgo)); + } + + await Assert.ThrowsAsync(() => Refresh(token)); + } } diff --git a/SW.Bitween.IntegrationTests/Tests/LongPromotedValuesTests.cs b/SW.Bitween.IntegrationTests/Tests/LongPromotedValuesTests.cs index 62d02359..2f452346 100644 --- a/SW.Bitween.IntegrationTests/Tests/LongPromotedValuesTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/LongPromotedValuesTests.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// A promoted value far longer than anything meant to be promoted — a path that picked up a notes +/// A promoted value far longer than anything meant to be promoted - a path that picked up a notes /// field, say. Its save used to fail after the delivery, so the bus retried the exchange and /// delivered it again, and the exchange never got a result. /// diff --git a/SW.Bitween.IntegrationTests/Tests/MapperEnrichmentBaselineTests.cs b/SW.Bitween.IntegrationTests/Tests/MapperEnrichmentBaselineTests.cs index 5fccd7a0..2701e5dd 100644 --- a/SW.Bitween.IntegrationTests/Tests/MapperEnrichmentBaselineTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/MapperEnrichmentBaselineTests.cs @@ -19,7 +19,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Partner and global values reach a Scriban template by being written into the payload as /// __partner__ and __globals__. Every template in production reads them from there, so -/// the enrichment is load-bearing behaviour rather than an implementation detail — which is why it +/// the enrichment is load-bearing behaviour rather than an implementation detail - which is why it /// gets pinned before the guard that skips it for context-aware mappers is written, not after. /// /// @@ -49,7 +49,7 @@ public MapperEnrichmentBaselineTests(BitweenFixture fixture) /// /// /// No handler is configured: RunHandler returns immediately on a null HandlerId, - /// which leaves the mapper's output as the last thing the pipeline produced — and it is already + /// which leaves the mapper's output as the last thing the pipeline produced - and it is already /// persisted, because RunMapper stores it as the file. /// private sealed record MapperRun(string? Output, string? Exception); @@ -115,7 +115,7 @@ private async Task RunMapperOnce( await xchangeService.Process("BaselineTest", $"{{\"Id\":\"{xchange.Id}\"}}"); // Process swallows any failure into the XchangeResult rather than rethrowing, and only - // writes the Output file when the mapper actually produced one — so a mapping that blew up + // writes the Output file when the mapper actually produced one - so a mapping that blew up // has no file to read, and the reason is on the result. // An XchangeResult's own Id is the exchange id it belongs to. var result = await db.FindAsync(xchange.Id); @@ -174,7 +174,7 @@ public async Task Partner_and_globals_are_injected_together() /// /// A root-array payload is not enriched, because the enrichment only runs when the payload - /// parses as a JObject — so a template reading __partner__ fails the exchange. + /// parses as a JObject - so a template reading __partner__ fails the exchange. /// /// /// The preview endpoint does inject into every element of a root array, so this mapping @@ -195,8 +195,8 @@ public async Task Root_array_payload_is_not_enriched_and_the_exchange_fails() } /// - /// A payload that is valid JSON but not an object — a receiver handing back a JSON-encoded - /// string — is not enriched either, and fails the same way. + /// A payload that is valid JSON but not an object - a receiver handing back a JSON-encoded + /// string - is not enriched either, and fails the same way. /// /// /// This is the case RunMapperEnrichmentTests was added for. It proves only that the @@ -221,7 +221,7 @@ public async Task Non_object_payload_is_not_enriched_and_the_exchange_fails() /// /// Deliberately does not assert that the payload is untouched. Enrichment reads every /// global values set in the database, and the fixture's database is shared across the whole - /// collection — so a set added by any other test would appear in this payload too. What the + /// collection - so a set added by any other test would appear in this payload too. What the /// added keys are called is pinned by the tests above; this one pins that adding them does not /// disturb the fields that were already there. /// diff --git a/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs b/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs index b5221739..9bc2859e 100644 --- a/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs @@ -19,7 +19,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// /// The last test here is the one that matters. The old mapper's preview built its own template and -/// assembled its own partner values, and the two drifted — a mapping using a partner property on a +/// assembled its own partner values, and the two drifted - a mapping using a partner property on a /// root-array payload previewed correctly and failed in production. Both paths now run the same /// three steps against the same context factory, and this pins that. /// @@ -49,7 +49,8 @@ private async Task PreviewAsync( object rules, string document, int? partnerId = null) { await using var scope = _fixture.CreateScope(); - var handler = new Preview(scope.ServiceProvider.GetRequiredService()); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); return await handler.Handle(new MappingPreviewRequest { @@ -95,7 +96,7 @@ public async Task Reports_each_failing_rule_separately() } /// - /// An unreadable document is a general error rather than a rule error — no rule is at fault, and + /// An unreadable document is a general error rather than a rule error - no rule is at fault, and /// the editor should say so above the document instead of marking rows. /// [Fact] @@ -120,7 +121,8 @@ public async Task An_empty_document_says_so() public async Task Unreadable_rules_are_a_general_error() { await using var scope = _fixture.CreateScope(); - var handler = new Preview(scope.ServiceProvider.GetRequiredService()); + scope.Superuser(); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); var response = await handler.Handle(new MappingPreviewRequest { @@ -137,7 +139,7 @@ public async Task An_unsupported_format_names_what_is_supported() // Named rather than a format that merely happens to be unsupported today: this test asked // for "csv" until delimited text landed, at which point it stopped testing anything and // failed on a null error message instead. Asserting the whole supported list is the half - // that has to be kept honest — the next format to arrive is meant to break this line, and + // that has to be kept honest - the next format to arrive is meant to break this line, and // it now says so out loud. var response = await PreviewAsync(new { version = 1, sourceFormat = "yaml" }, "{}"); @@ -169,7 +171,7 @@ public async Task An_unsupported_format_names_what_is_supported() /// /// The writer's own tests prove it honours a delimiter and a header. This pins that the ones on - /// the rules are the ones it is handed — the preview builds its target format from them. + /// the rules are the ones it is handed - the preview builds its target format from them. /// [Fact] public async Task A_delimited_file_is_written_with_the_delimiter_and_header_the_rules_ask_for() @@ -228,7 +230,46 @@ public async Task Partner_values_are_available_when_a_partner_is_named() } /// - /// The same rules, the same document, the same partner — through the preview endpoint and + /// The rules are the caller's own, so they can copy any value into the output. A value the + /// partner's owner declared secret comes back as the sentinel, not as the credential. + /// + [Fact] + public async Task A_partner_value_declared_secret_is_masked() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var partner = new Partner("Preview Secret Partner") + { + AdapterProperties = new Dictionary + { + ["region-code"] = "JO", + ["api-key"] = "partner-secret-value", + }, + SecretProperties = ["api-key"], + }; + db.Set().Add(partner); + await db.SaveChangesAsync(); + + var response = await PreviewAsync(new + { + version = 1, + fields = new object[] + { + new { target = new[] { "region" }, from = new { kind = "Partner", key = "region-code" } }, + new { target = new[] { "key" }, from = new { kind = "Partner", key = "api-key" } }, + }, + }, "{}", partner.Id); + + Assert.Null(response.Error); + var output = JObject.Parse(response.OutputDocument!); + Assert.Equal("JO", output["region"]?.ToString()); + Assert.Equal(AdapterSecretProperties.Sentinel, output["key"]?.ToString()); + Assert.DoesNotContain("partner-secret-value", response.OutputDocument); + } + + /// + /// The same rules, the same document, the same partner - through the preview endpoint and /// through a real exchange. Byte for byte. /// /// diff --git a/SW.Bitween.IntegrationTests/Tests/MetricsTests.cs b/SW.Bitween.IntegrationTests/Tests/MetricsTests.cs new file mode 100644 index 00000000..a90826ea --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/MetricsTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.Bitween.Services.Metrics; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Metrics end to end against a real database: an exchange run through the pipeline lands in its +/// hourly row and its adapter's, its result says where and how it ran, and history from before +/// metrics existed is folded in exactly once however many times the backfill runs. +/// +[Collection("Bitween")] +public class MetricsTests(BitweenFixture fixture) +{ + private const string Responder = nameof(Adapters.NativeTestResponder); + private static int _seq; + private static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}-{Guid.NewGuid():N}"[..40]; + + async Task SubscriptionAsync() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var doc = new Document(null, Unique("Metrics type"), DocumentFormat.Other); + db.Add(doc); + await db.SaveChangesAsync(); + var sub = new Subscription(Unique("Metrics job"), doc.Id) { Inactive = false, HandlerId = Responder }; + db.Add(sub); + await db.SaveChangesAsync(); + return sub.Id; + } + + async Task RunExchangeAsync(int subscriptionId, Func beforeProcessing = null) + { + string id; + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + id = await scope.ServiceProvider.GetRequiredService() + .SubmitSubscriptionXchange(subscriptionId, new XchangeFile("hello", "m.txt")); + } + if (beforeProcessing != null) + await using (var scope = fixture.CreateScope()) + await beforeProcessing(scope.ServiceProvider.GetRequiredService(), id); + await using (var scope = fixture.CreateScope()) + await scope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = id })); + return id; + } + + [Fact] + public async Task A_processed_exchange_is_counted_for_its_subscription_and_its_handler() + { + var subscriptionId = await SubscriptionAsync(); + var xchangeId = await RunExchangeAsync(subscriptionId); + await using var scope = fixture.CreateScope(); + await scope.ServiceProvider.GetRequiredService().FlushAsync(CancellationToken.None); + var db = scope.ServiceProvider.GetRequiredService(); + var row = await db.Set().AsNoTracking() + .SingleAsync(s => s.SubscriptionId == subscriptionId && s.Node == MetricsRecorder.Node); + Assert.Equal((1, 1, 0), (row.Total, row.Succeeded, row.Failed)); + Assert.Equal(5, row.BytesIn); + + var calls = await db.Set().AsNoTracking() + .Where(s => s.AdapterId == Responder.ToLowerInvariant() && s.Role == "handler").SumAsync(s => s.Calls); + Assert.True(calls >= 1); + + // The result says which node ran it and how long it took. + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == xchangeId); + Assert.Equal(MetricsRecorder.Node, result.NodeName); + Assert.NotNull(result.DurationMs); + } + + [Fact] + public async Task History_is_folded_in_once_however_many_times_the_backfill_runs() + { + var subscriptionId = await SubscriptionAsync(); + var past = MetricsRecorder.HourOf(DateTime.UtcNow).AddHours(-3).AddMinutes(20); + // An exchange from before metrics were recorded: started three hours ago, finished then. + await RunExchangeAsync(subscriptionId, async (db, id) => + await db.Set().Where(x => x.Id == id).ExecuteUpdateAsync(u => u.SetProperty(x => x.StartedOn, past))); + await using (var scope = fixture.CreateScope()) + await scope.ServiceProvider.GetRequiredService().Set() + .Where(s => s.SubscriptionId == subscriptionId).ExecuteDeleteAsync(); + + await using var services = fixture.CreateScope(); + var backfill = new MetricsBackfill(services.ServiceProvider.GetRequiredService(), + new BitweenOptions { MetricsBackfillDays = 1 }, NullLogger.Instance); + MetricsBackfill.PauseBetweenHours = TimeSpan.Zero; + await backfill.RunAsync(CancellationToken.None); + await backfill.RunAsync(CancellationToken.None); + + await using var check = fixture.CreateScope(); + var rows = await check.ServiceProvider.GetRequiredService().Set().AsNoTracking() + .Where(s => s.SubscriptionId == subscriptionId && s.Node == ExchangeStats.HistoryNode).ToListAsync(); + var row = Assert.Single(rows); + Assert.Equal(MetricsRecorder.HourOf(past), DateTime.SpecifyKind(row.Hour, DateTimeKind.Utc)); + Assert.Equal((1, 1), (row.Total, row.Succeeded)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/MicrosoftSignInTests.cs b/SW.Bitween.IntegrationTests/Tests/MicrosoftSignInTests.cs new file mode 100644 index 00000000..4f4ade27 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/MicrosoftSignInTests.cs @@ -0,0 +1,137 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Threading; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Signing in with Microsoft through the whole login handler, against the real database. The +/// tokens are signed here, with the key the handler is told Microsoft publishes; everything after +/// that - matching the account, binding the Microsoft identity to it, refusing another identity or +/// a disabled account - is the code that runs in production. +/// +[Collection("Bitween")] +public class MicrosoftSignInTests(BitweenFixture fixture) : IDisposable +{ + const string ClientId = "11111111-1111-1111-1111-111111111111"; + const string Tenant = "22222222-2222-2222-2222-222222222222"; + static readonly RsaSecurityKey Key = new(RSA.Create(2048)) { KeyId = "test" }; + static int _seq; + + readonly Func>> realKeys = + SwapKeys(); + readonly (string ClientId, string TenantId) realSettings = Configure(fixture); + + static Func>> SwapKeys() + { + var real = AccountExtensions.MicrosoftSigningKeys; + AccountExtensions.MicrosoftSigningKeys = () => Task.FromResult>([Key]); + return real; + } + + static (string, string) Configure(BitweenFixture fixture) + { + using var scope = fixture.App.Services.CreateScope(); + var options = scope.ServiceProvider.GetRequiredService(); + var before = (options.MsalClientId, options.MsalTenantId); + options.MsalClientId = ClientId; + options.MsalTenantId = Tenant; + return before; + } + + public void Dispose() + { + AccountExtensions.MicrosoftSigningKeys = realKeys; + using var scope = fixture.App.Services.CreateScope(); + var options = scope.ServiceProvider.GetRequiredService(); + (options.MsalClientId, options.MsalTenantId) = realSettings; + } + + static string Token(string email, string oid, string tenant = Tenant) => + new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken( + issuer: $"https://login.microsoftonline.com/{tenant}/v2.0", + audience: ClientId, + claims: [new Claim("preferred_username", email), new Claim("oid", oid), new Claim("tid", tenant)], + notBefore: DateTime.UtcNow.AddMinutes(-5), + expires: DateTime.UtcNow.AddMinutes(30), + signingCredentials: new SigningCredentials(Key, SecurityAlgorithms.RsaSha256))); + + async Task SignIn(string msToken) + { + await using var scope = fixture.CreateScope(); + var accessor = scope.ServiceProvider.GetRequiredService(); + accessor.HttpContext = new DefaultHttpContext(); + accessor.HttpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("203.0.113.9"); + var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + return await handler.Handle(new UserLogin { MsToken = msToken }); + } + + async Task Member(bool disabled = false) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + // No password: a Microsoft-only member exists purely to be matched. + var account = new Account("Microsoft Member", $"ms{Interlocked.Increment(ref _seq)}-{Guid.NewGuid():N}@example.com"[..36], + null, AccountRole.Member); + if (disabled) account.SetDisabled(true); + db.Add(account); + await db.SaveChangesAsync(); + return account; + } + + async Task BoundIdentity(int accountId) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService().Set() + .Where(a => a.Id == accountId).Select(a => a.MicrosoftIdentity).SingleAsync(); + } + + [Fact] + public async Task The_first_sign_in_binds_the_Microsoft_identity_which_then_signs_in_whatever_its_address() + { + var account = await Member(); + var oid = Guid.NewGuid().ToString(); + + Assert.NotNull(await SignIn(Token(account.Email, oid))); + Assert.Equal($"{oid}@{Tenant}", await BoundIdentity(account.Id)); + + // Renamed at Microsoft: the binding, not the address, says who this is. + Assert.NotNull(await SignIn(Token("renamed@example.com", oid))); + } + + [Fact] + public async Task Another_Microsoft_identity_with_the_same_address_is_refused_once_one_is_bound() + { + var account = await Member(); + await SignIn(Token(account.Email, Guid.NewGuid().ToString())); + + var refused = await Assert.ThrowsAsync(() => SignIn(Token(account.Email, Guid.NewGuid().ToString()))); + Assert.Contains("different Microsoft account", refused.Message); + } + + [Fact] + public async Task A_disabled_account_and_a_token_from_another_tenant_are_refused() + { + var disabled = await Member(disabled: true); + Assert.Contains("disabled", (await Assert.ThrowsAsync(() => + SignIn(Token(disabled.Email, Guid.NewGuid().ToString())))).Message); + + var account = await Member(); + await Assert.ThrowsAsync(() => + SignIn(Token(account.Email, Guid.NewGuid().ToString(), tenant: "33333333-3333-3333-3333-333333333333"))); + Assert.Null(await BoundIdentity(account.Id)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/MySqlAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/MySqlAdapterTests.cs index fd86bd88..bae58abb 100644 --- a/SW.Bitween.IntegrationTests/Tests/MySqlAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/MySqlAdapterTests.cs @@ -17,7 +17,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// The same suite as PostgreSQL's and Oracle's, which is the point: three engines run on one core, /// so the parts that are shared should behave identically and the parts that differ should differ -/// VISIBLY — in the capability list, and in how a routine returns rows. Everything here goes over +/// VISIBLY - in the capability list, and in how a routine returns rows. Everything here goes over /// the resident transport into a real adapter process and out to a real database. /// [Collection("Bitween")] @@ -79,7 +79,7 @@ public async Task Connection_test_reports_each_stage_and_prepares_every_statemen /// /// Where MySQL is honestly different from the other two. A procedure returns rows by SELECTing, - /// which PostgreSQL cannot do — but there is no MERGE, no RETURNING, no array type and no + /// which PostgreSQL cannot do - but there is no MERGE, no RETURNING, no array type and no /// sequence, and claiming any of them would have a statement written against it fail on a real /// server. /// @@ -104,7 +104,7 @@ public async Task Describe_reports_the_engine_and_where_it_differs() var objects = described["supportedObjects"]!.Select(o => o.Value()).ToList(); Assert.Contains("procedure", objects); Assert.Contains("function", objects); - // No sequences in MySQL — AUTO_INCREMENT belongs to a column, not to an object. + // No sequences in MySQL - AUTO_INCREMENT belongs to a column, not to an object. Assert.DoesNotContain("sequence", objects); // READ UNCOMMITTED does something here, unlike PostgreSQL where it is silently promoted. @@ -142,13 +142,13 @@ public async Task Discover_finds_the_table_with_its_columns_and_key() Assert.True(columns[0].Value("primaryKey")); Assert.False(columns[0].Value("nullable")); - // tinyint(1) IS the boolean type in MySQL, and the driver returns one — so reporting "int" + // tinyint(1) IS the boolean type in MySQL, and the driver returns one - so reporting "int" // would mislead whoever writes the mapper. var processed = columns.Single(c => c.Value("name") == "processed"); Assert.Equal("bool", processed.Value("clrType")); // A CURRENT_TIMESTAMP default reports as DEFAULT_GENERATED in `extra`, which is a value the - // database fills in — the only distinction an insert cares about. + // database fills in - the only distinction an insert cares about. var created = columns.Single(c => c.Value("name") == "created_at"); Assert.True(created.Value("generated")); @@ -262,7 +262,7 @@ public async Task Execute_reports_what_it_changed() /// /// A batch is one transaction: either every statement in it happened or none did. Proven by - /// making the second one fail — a duplicate primary key — and then looking for the first. + /// making the second one fail - a duplicate primary key - and then looking for the first. /// [SkippableFact] public async Task A_failed_batch_rolls_back_what_came_before_it() @@ -300,7 +300,7 @@ public async Task A_failed_batch_rolls_back_what_came_before_it() /// /// Ad-hoc SQL is refused unless the data source allows it, whatever the engine. The mapper is a - /// template over message content — if it can emit SQL text, every Xchange is an injection + /// template over message content - if it can emit SQL text, every Xchange is an injection /// vector into the customer's database. /// [SkippableFact] diff --git a/SW.Bitween.IntegrationTests/Tests/NativeAdapterTimeoutTests.cs b/SW.Bitween.IntegrationTests/Tests/NativeAdapterTimeoutTests.cs new file mode 100644 index 00000000..0ca26972 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/NativeAdapterTimeoutTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using SW.Bitween.Services.Adapters; +using SW.Bitween.Services.Metrics; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// A built-in adapter's call that never returns: it used to hold its place in the processing queue for +/// good (an SFTP server that signed in and said nothing stopped all of Bitween's processing in the lab). +/// Now it fails as a timeout after Bitween:NativeAdapterTimeout, and what it holds open is closed. +/// +public class NativeAdapterTimeoutTests +{ + sealed class Held : IDisposable + { + public bool Disposed { get; private set; } + public void Dispose() => Disposed = true; + } + + [Fact] + public async Task A_call_that_never_returns_fails_as_a_timeout_and_its_adapter_is_released() + { + var adapter = new Held(); + var never = new TaskCompletionSource().Task; + var clock = Stopwatch.StartNew(); + + var failure = await Assert.ThrowsAsync(() => + NativeAdapterRuntime.Within(never, TimeSpan.FromMilliseconds(200), adapter, "nativesftpuploadhandler", "Handle")); + + Assert.InRange(clock.Elapsed, TimeSpan.FromMilliseconds(150), TimeSpan.FromSeconds(10)); + Assert.Contains("nativesftpuploadhandler did not finish Handle within", failure.Message); + Assert.True(adapter.Disposed); + Assert.Equal(AdapterFailure.Timeout, AdapterInvoker.Classify(failure)); + } + + [Fact] + public async Task A_call_that_finishes_in_time_or_has_no_limit_is_untouched() + { + var adapter = new Held(); + Assert.Equal("done", await NativeAdapterRuntime.Within(Task.FromResult("done"), TimeSpan.FromSeconds(5), adapter, "x", "Handle")); + Assert.Equal("slow", await NativeAdapterRuntime.Within(Delayed("slow", 300), null, adapter, "x", "Handle")); + Assert.False(adapter.Disposed); + + // The adapter's own failure is its own, not a timeout. + var own = await Assert.ThrowsAsync(() => + NativeAdapterRuntime.Within(Task.FromException(new InvalidOperationException("refused")), TimeSpan.FromSeconds(5), adapter, "x", "Handle")); + Assert.Equal("refused", own.Message); + } + + static async Task Delayed(string value, int ms) + { + await Task.Delay(ms); + return value; + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/NativeMapperExchangeTests.cs b/SW.Bitween.IntegrationTests/Tests/NativeMapperExchangeTests.cs index 4195037b..99672425 100644 --- a/SW.Bitween.IntegrationTests/Tests/NativeMapperExchangeTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/NativeMapperExchangeTests.cs @@ -18,7 +18,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// supplied by XchangeService, output written to the exchange's Output file. /// /// -/// The unit tests cover the engine directly. These cover the wiring around it — that the rules +/// The unit tests cover the engine directly. These cover the wiring around it - that the rules /// survive the round trip through the database, that the context arrives, and that a payload which /// is not JSON reaches the mapper at all, which was impossible before the enrichment guard. /// @@ -135,7 +135,7 @@ public async Task Reports_the_content_type_of_the_format_it_produced() } /// - /// Partner and global values arrive as context rather than being read out of the payload — and + /// Partner and global values arrive as context rather than being read out of the payload - and /// the payload here has no room for them, being a root array. /// [Fact] @@ -159,7 +159,7 @@ public async Task Partner_and_global_values_arrive_as_context() Assert.Null(run.Exception); var output = JObject.Parse(run.Output!); - // Both of these are null under NativeJSONMapper for a root-array payload — the enrichment + // Both of these are null under NativeJSONMapper for a root-array payload - the enrichment // only touches a JSON object, so the same mapping fails there. See MapperEnrichmentBaselineTests. Assert.Equal("JO", output["region"]?.ToString()); Assert.Equal("WEB", output["channel"]?.ToString()); @@ -319,7 +319,7 @@ public async Task A_written_entry_comes_before_the_walked_ones() } /// - /// A list that walks nothing is exactly the entries written into it — one slot per rule, + /// A list that walks nothing is exactly the entries written into it - one slot per rule, /// which is what the previous mapper called a primitive array. /// [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/NotifierTests.cs b/SW.Bitween.IntegrationTests/Tests/NotifierTests.cs index d4c88887..c960cff7 100644 --- a/SW.Bitween.IntegrationTests/Tests/NotifierTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/NotifierTests.cs @@ -12,7 +12,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// Notifiers — the alerts sent when an exchange succeeds or fails. +/// Notifiers - the alerts sent when an exchange succeeds or fails. /// /// /// A notifier points at the integrations it watches rather than the other way round, so nothing @@ -72,7 +72,7 @@ public async Task Updating_with_no_handler_properties_clears_them_rather_than_fa Assert.Single((await Stored(id)).HandlerProperties); // An absent list means none, as it does for a document's promoted properties. Left - // implicit this threw ArgumentNullException — a 500 for a well-formed request. + // implicit this threw ArgumentNullException - a 500 for a well-formed request. await Update(id, new NotifierUpdate { Name = Unique("No settings"), HandlerId = "native:smtp" }); Assert.Empty((await Stored(id)).HandlerProperties); @@ -126,7 +126,7 @@ public async Task The_watch_list_is_replaced_by_what_the_edit_sends() RunOnSubscriptions = [new NotifierSubscription { Id = second }], }); - // The list is a replacement, not an addition — otherwise an integration can never be + // The list is a replacement, not an addition - otherwise an integration can never be // taken off a notifier once added. Assert.Equal([second], (await Stored(id)).RunOnSubscriptions); } @@ -167,11 +167,86 @@ public async Task Deleting_a_notifier_takes_its_watch_list_with_it() var checkDb = check.ServiceProvider.GetRequiredService(); Assert.False(await checkDb.Set().AnyAsync(n => n.Id == id)); - // The watch list is the notifier's own column, so it goes with it — but the integration + // The watch list is the notifier's own column, so it goes with it - but the integration // it named is configuration in its own right and must survive. Assert.True(await checkDb.Set().AnyAsync(s => s.Id == subscriptionId)); } + /// + /// The SMTP password is [Secure] on the handler, so a notifier returns it masked like a + /// subscription does, and sending the mask back keeps it. + /// + [Fact] + public async Task A_handler_secret_is_masked_and_kept_when_sent_back() + { + var id = await Create(Unique("Secret notifier")); + await Update(id, new NotifierUpdate + { + Name = Unique("Secret notifier"), + HandlerId = nameof(SW.Bitween.NativeAdapters.SmtpHandler.NativeSmtpHandler), + HandlerProperties = + [ + new KeyAndValue { Key = "Host", Value = "smtp.example.com" }, + new KeyAndValue { Key = "Password", Value = "smtp-secret-value" }, + ], + }); + + Newtonsoft.Json.Linq.JObject read; + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + var get = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + read = Newtonsoft.Json.Linq.JObject.FromObject(await get.Handle(id)); + } + + var returned = read["HandlerProperties"]!.ToDictionary(p => (string)p["Key"]!, p => (string)p["Value"]!); + Assert.Equal("smtp.example.com", returned["Host"]); + Assert.Equal(AdapterSecretProperties.Sentinel, returned["Password"]); + + // The form sends back what it was given. + await Update(id, new NotifierUpdate + { + Name = Unique("Secret notifier"), + HandlerId = nameof(SW.Bitween.NativeAdapters.SmtpHandler.NativeSmtpHandler), + HandlerProperties = returned.Select(kv => new KeyAndValue { Key = kv.Key, Value = kv.Value }).ToList(), + }); + Assert.Equal("smtp-secret-value", (await Stored(id)).HandlerProperties["Password"]); + } + + /// + /// Pointing the notifier at another mail server means entering the password again: kept, it + /// would be sent to whoever runs that server, by someone who never saw it. + /// + [Fact] + public async Task Changing_the_server_drops_the_kept_secret() + { + var handler = nameof(SW.Bitween.NativeAdapters.SmtpHandler.NativeSmtpHandler); + var id = await Create(Unique("Moved notifier")); + await Update(id, new NotifierUpdate + { + Name = Unique("Moved notifier"), + HandlerId = handler, + HandlerProperties = + [ + new KeyAndValue { Key = "Host", Value = "smtp.example.com" }, + new KeyAndValue { Key = "Password", Value = "smtp-secret-value" }, + ], + }); + + await Update(id, new NotifierUpdate + { + Name = Unique("Moved notifier"), + HandlerId = handler, + HandlerProperties = + [ + new KeyAndValue { Key = "Host", Value = "smtp.attacker.example" }, + new KeyAndValue { Key = "Password", Value = AdapterSecretProperties.Sentinel }, + ], + }); + + Assert.False((await Stored(id)).HandlerProperties.ContainsKey("Password")); + } + [Fact] public async Task A_viewer_cannot_create_or_delete_a_notifier() { diff --git a/SW.Bitween.IntegrationTests/Tests/OracleAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/OracleAdapterTests.cs index 835c606a..bd0f66be 100644 --- a/SW.Bitween.IntegrationTests/Tests/OracleAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/OracleAdapterTests.cs @@ -18,7 +18,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// Nothing here is mocked: a container comes up, a schema is created in it, the adapter is /// installed from cloud storage and spawned as its own process, and every assertion goes over the /// resident transport into that process and out to the database. That is the only way to test the -/// parts that actually break — REF CURSOR binding, the data dictionary queries, and a cursor that +/// parts that actually break - REF CURSOR binding, the data dictionary queries, and a cursor that /// has to survive the adapter process being restarted underneath it. /// /// The container and the adapter are started once for the class. Tests are written so they do not @@ -164,7 +164,7 @@ public async Task Discover_lists_sequences_with_their_current_value() Assert.NotNull(sequence.Value("rowCount")); } - /// Procedure arguments, with a REF CURSOR called out as one — a caller must bind it differently. + /// Procedure arguments, with a REF CURSOR called out as one - a caller must bind it differently. [SkippableFact] public async Task Discover_reports_a_ref_cursor_argument_as_such() { @@ -233,7 +233,7 @@ public async Task An_unknown_statement_name_says_which_ones_exist() /// /// Paging, and specifically that it loses nothing. Finding out whether there is another page - /// means reading a row, and a reader cannot be rewound — so that row has to be carried into the + /// means reading a row, and a reader cannot be rewound - so that row has to be carried into the /// next page rather than dropped. Dropped, it costs exactly one row per page, which surfaces /// months later as a single missing order and is close to unfindable. /// @@ -302,7 +302,7 @@ public async Task A_write_reports_what_it_changed() /// /// The case that forces an Oracle-specific hook to exist at all. An Oracle procedure does not - /// return rows the way every other engine's does — it returns them through a REF CURSOR out + /// return rows the way every other engine's does - it returns them through a REF CURSOR out /// parameter the caller has to declare, and a plain output parameter gets an ORA-06550 about /// argument types that points nowhere near the real problem. /// @@ -367,7 +367,7 @@ public async Task A_failing_batch_rolls_the_whole_thing_back() /// rather than replaying from the beginning. /// /// One test rather than three, because they all consume from the same cursor and xUnit gives no - /// ordering — split up, they would race each other for the rows. + /// ordering - split up, they would race each other for the rows. /// [SkippableFact] public async Task The_receiver_advances_a_cursor_that_survives_a_restart() @@ -392,12 +392,12 @@ public async Task The_receiver_advances_a_cursor_that_survives_a_restart() properties: me); // XchangeFile is SW.PrimitiveTypes' type, not one of the contracts this adapter - // controls, so its casing is whatever that library serialises — read it either way + // controls, so its casing is whatever that library serialises - read it either way // rather than pinning a shape we do not own. var data = file.Value("data") ?? file.Value("Data"); Assert.False(string.IsNullOrWhiteSpace(data)); - // What the pipeline calls once Bitween has durably accepted the row — and therefore the + // What the pipeline calls once Bitween has durably accepted the row - and therefore the // only point at which the cursor may move. await adapter.InvokeAsync("DeleteFile", id, timeoutSeconds: 120, properties: me); @@ -430,8 +430,8 @@ await adapter.InvokeAsync("DeleteFile", id, timeoutSeconds: 120, // ---------------------------------------------------------------- validating /// - /// ODP.NET's Prepare is a client-side no-op — Oracle compiles a statement when it is executed, - /// not when it is prepared — so the shared check passed EVERYTHING here, including a select + /// ODP.NET's Prepare is a client-side no-op - Oracle compiles a statement when it is executed, + /// not when it is prepared - so the shared check passed EVERYTHING here, including a select /// from a table that does not exist. These pin the replacement: DBMS_SQL.PARSE, which compiles /// and resolves names without running anything. /// @@ -474,7 +474,7 @@ public async Task A_syntax_error_is_caught() Assert.False(result.Value("ok")); // @ is a database link on Oracle, not a placeholder, so this is ORA-00936 missing - // expression — and the hint says to write :id instead. + // expression - and the hint says to write :id instead. Assert.Contains(":id", result.Value("error")); } @@ -546,7 +546,7 @@ async Task StartAdapterAsync() { AdapterId = BusAdapters.Oracle, - // The data source id, exactly as the supervisor keys it — which is what lets the + // The data source id, exactly as the supervisor keys it - which is what lets the // Inspect endpoint find this instance, and what scopes its host-held state. InstanceKey = _dataSourceId.ToString() }; diff --git a/SW.Bitween.IntegrationTests/Tests/OutboxTests.cs b/SW.Bitween.IntegrationTests/Tests/OutboxTests.cs new file mode 100644 index 00000000..d4a19ab3 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/OutboxTests.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Adapters; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Messages are written to the outbox with the change that raised them, published straight after +/// the commit, and published later by the dispatcher when that fails. Delivery is therefore at +/// least once, and processing has to shrug off a repeat. +/// +[Collection("Bitween")] +public class OutboxTests(BitweenFixture fixture) +{ + [Fact] + public async Task A_new_exchange_is_announced_through_the_outbox() + { + var (xchangeId, _) = await CreateExchangeAsync(); + + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var row = await db.Set().AsNoTracking() + .Where(m => m.Body.Contains(xchangeId)) + .SingleAsync(); + + // Published straight after the commit, as messages always were; the row is the record. + Assert.NotNull(row.PublishedOn); + } + + [Fact] + public async Task A_message_left_unpublished_is_published_by_the_dispatcher() + { + long id; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var message = new OutboxMessage("OutboxTestMessage", JsonConvert.SerializeObject(new { Id = Guid.NewGuid() })); + db.Add(message); + await db.SaveChangesAsync(); + id = message.Id; + + // Back to unpublished, and old enough for the dispatcher: the state a broker outage at + // commit time leaves behind. + var longAgo = DateTime.UtcNow.AddMinutes(-5); + await db.Set().Where(m => m.Id == id) + .ExecuteUpdateAsync(u => u + .SetProperty(m => m.CreatedOn, longAgo) + .SetProperty(m => m.PublishedOn, (DateTime?)null)); + } + + var dispatcher = ActivatorUtilities.CreateInstance(fixture.App.Services); + await dispatcher.DispatchOnceAsync(CancellationToken.None); + + await using var check = fixture.CreateScope(); + var row = await check.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(m => m.Id == id); + Assert.NotNull(row.PublishedOn); + } + + /// + /// A repeat delivery - after a crash between commit and ack, or the dispatcher publishing a + /// message whose first publish had gone through - used to run the handler again and deliver + /// to the partner twice. + /// + [Fact] + public async Task A_repeat_delivery_of_a_processed_exchange_does_not_deliver_again() + { + var (xchangeId, token) = await CreateExchangeAsync(); + + for (var delivery = 0; delivery < 2; delivery++) + { + await using var scope = fixture.CreateScope(); + await scope.ServiceProvider.GetRequiredService() + .Process("OutboxRepeat", JsonConvert.SerializeObject(new { Id = xchangeId })); + } + + Assert.Equal(1, NativeTestResponder.Deliveries.GetValueOrDefault(token)); + + await using var check = fixture.CreateScope(); + var result = await check.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(r => r.Id == xchangeId); + Assert.True(result.Success, result.Exception); + } + + private async Task<(string XchangeId, string Token)> CreateExchangeAsync() + { + var token = $"outbox-{Guid.NewGuid():N}"; + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var document = new Document(null, $"Outbox {Guid.NewGuid():N}", DocumentFormat.Json); + db.Add(document); + await db.SaveChangesAsync(); + + var subscription = new Subscription($"Outbox {Guid.NewGuid():N}", document.Id) + { Inactive = false, HandlerId = nameof(NativeTestResponder) }; + subscription.SetDictionaries( + new Dictionary { ["Body"] = token }, + new Dictionary(), new Dictionary(), + new Dictionary(), new Dictionary()); + db.Add(subscription); + await db.SaveChangesAsync(); + scope.ServiceProvider.GetRequiredService().Revoke(); + + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(subscription, new XchangeFile("{}")); + await db.SaveChangesAsync(); + return (xchange.Id, token); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/PartnerAndGlobalSecretTests.cs b/SW.Bitween.IntegrationTests/Tests/PartnerAndGlobalSecretTests.cs index c5db4a71..4b175fd8 100644 --- a/SW.Bitween.IntegrationTests/Tests/PartnerAndGlobalSecretTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PartnerAndGlobalSecretTests.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// response while leaving the {{partner.…}} / {{globals.…}} reference to it usable. /// /// -/// Until this existed the two were the only property bags in the app served in clear — a data +/// Until this existed the two were the only property bags in the app served in clear - a data /// source masks, a subscription's adapter properties mask, and these did not, which made them the /// worst place to keep a credential and the only place operators could share one. The flag is /// declared rather than guessed from the key's name: these values have always been readable, so @@ -29,7 +29,7 @@ public class PartnerAndGlobalSecretTests(BitweenFixture fixture) /// /// The whole point: a locked value does not come back. Get is the only endpoint that ever - /// served a partner's property values — Search has always sent names alone. + /// served a partner's property values - Search has always sent names alone. /// [Fact] public async Task A_locked_partner_property_is_never_returned_in_clear() @@ -47,7 +47,7 @@ public async Task A_locked_partner_property_is_never_returned_in_clear() /// /// Nothing is hidden until someone says so. A property whose name reads like a credential is - /// still served in clear, because it always was — the alternative silently blinds an operator + /// still served in clear, because it always was - the alternative silently blinds an operator /// on the day they upgrade. /// [Fact] @@ -183,7 +183,7 @@ public async Task A_locked_global_value_is_masked_and_its_neighbours_are_not() } /// - /// The list endpoint feeds the reference picker, which is why it sends values at all — so it + /// The list endpoint feeds the reference picker, which is why it sends values at all - so it /// is also a way out for a locked one. The key stays, because a secret nobody can point at is /// no use; only the value goes. /// @@ -210,11 +210,12 @@ public async Task The_list_endpoint_masks_a_locked_value_but_keeps_its_key() public async Task Saving_a_masked_global_value_back_keeps_the_stored_value() { var id = await CreateGlobalSetAsync( - new Dictionary { ["baseUrl"] = "https://api.example.com", ["token"] = "glb-7" }, + new Dictionary + { ["baseUrl"] = "https://api.example.com", ["region"] = "eu", ["token"] = "glb-7" }, secretProperties: ["token"]); var row = await GetGlobalSetAsync(id); - row.Values["baseUrl"] = "https://api2.example.com"; + row.Values["region"] = "me"; await UpdateGlobalSetAsync(id, row); await using var scope = fixture.CreateScope(); @@ -222,7 +223,29 @@ public async Task Saving_a_masked_global_value_back_keeps_the_stored_value() var stored = await db.Set().AsNoTracking().SingleAsync(x => x.Id == id); Assert.Equal("glb-7", stored.Values["token"]); - Assert.Equal("https://api2.example.com", stored.Values["baseUrl"]); + Assert.Equal("me", stored.Values["region"]); + } + + /// + /// Except when the edit changes where the values point: the token would go wherever the new + /// address leads, sent by someone who never saw it, so it has to be entered again. + /// + [Fact] + public async Task Pointing_a_global_set_elsewhere_drops_its_masked_secret() + { + var id = await CreateGlobalSetAsync( + new Dictionary { ["baseUrl"] = "https://api.example.com", ["token"] = "glb-8" }, + secretProperties: ["token"]); + + var row = await GetGlobalSetAsync(id); + row.Values["baseUrl"] = "https://api.attacker.example"; + await UpdateGlobalSetAsync(id, row); + + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var stored = await db.Set().AsNoTracking().SingleAsync(x => x.Id == id); + + Assert.False(stored.Values.ContainsKey("token")); } // ------------------------------------------------------------------- helpers diff --git a/SW.Bitween.IntegrationTests/Tests/PartnerKeyHashTests.cs b/SW.Bitween.IntegrationTests/Tests/PartnerKeyHashTests.cs new file mode 100644 index 00000000..f2434645 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/PartnerKeyHashTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Services; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Partner API keys are stored as hashes, so a copy of the database is not every partner's key. +/// Keys stored before this are converted at startup, and still work until they are. +/// +[Collection("Bitween")] +public class PartnerKeyHashTests(BitweenFixture fixture) +{ + [Fact] + public async Task A_key_is_stored_as_its_hash_and_still_authorises() + { + var key = Guid.NewGuid().ToString("N"); + var partnerId = await CreatePartnerAsync(key); + + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var stored = (await db.Set().AsNoTracking().SingleAsync(p => p.Id == partnerId)).ApiCredentials.Single(); + + Assert.Equal(PartnerKeyHash.Of(key), stored.Key); + Assert.Equal(key[..5], stored.KeyPrefix); + Assert.True(await AuthorisesAsync(key)); + } + + [Fact] + public async Task A_key_stored_before_hashing_works_and_is_converted_at_startup() + { + var key = Guid.NewGuid().ToString("N"); + var partnerId = await CreatePartnerAsync(key); + + // The state an upgraded database starts in: the key itself in the column. + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.ExecuteSqlRawAsync( + "UPDATE infolink.partner_api_credential SET key = {0}, key_prefix = NULL WHERE partner_id = {1}", + key, partnerId); + } + + Assert.True(await AuthorisesAsync(key)); + + await using (var scope = fixture.CreateScope()) + await PartnerKeyHashing.HashStoredKeys( + scope.ServiceProvider.GetRequiredService(), NullLogger.Instance); + + await using var check = fixture.CreateScope(); + var stored = (await check.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(p => p.Id == partnerId)).ApiCredentials.Single(); + Assert.Equal(PartnerKeyHash.Of(key), stored.Key); + Assert.True(await AuthorisesAsync(key)); + } + + [Fact] + public async Task The_hash_itself_is_not_a_key() + { + var key = Guid.NewGuid().ToString("N"); + await CreatePartnerAsync(key); + + // Someone holding a copy of the table has the hash. Presenting it must get them nowhere. + Assert.False(await AuthorisesAsync(PartnerKeyHash.Of(key))); + } + + private async Task CreatePartnerAsync(string key) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner($"Hashed {Guid.NewGuid():N}"); + partner.SetApiCredentials([new ApiCredential("main", key)]); + db.Add(partner); + await db.SaveChangesAsync(); + return partner.Id; + } + + /// + /// Asks the partner-facing exchange lookup, which authorises the key before anything else: a + /// refused key is an unauthorised error, an accepted one goes on to look for an exchange. + /// + private async Task AuthorisesAsync(string key) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Set( + new ClaimsPrincipal(new ClaimsIdentity()), + [new RequestValue("partnerkey", key, RequestValueType.HttpHeader)]); + var get = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + try + { + await get.Handle("no-such-exchange"); + return true; + } + catch (SWUnauthorizedException) + { + return false; + } + catch (Exception) + { + return true; + } + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs index b9d97bf3..ff8a6908 100644 --- a/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs @@ -11,11 +11,11 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// {{partner.…}} in an adapter's properties has to be replaced by the time the Xchange is -/// written, because that is the copy the handler runs on — nothing resolves it later. +/// written, because that is the copy the handler runs on - nothing resolves it later. /// /// /// Every caller of CreateXchange that has a partner in hand (a bus gateway route, an API -/// gateway call) passed one, and every caller that didn't left the token literal — so a handler +/// gateway call) passed one, and every caller that didn't left the token literal - so a handler /// posted to the URL {{partner.webhookUrl}}. The subscription's own partner was sitting /// on the subscription the whole time; these tests pin down that it is now used. /// diff --git a/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs b/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs index 25679010..e989bef1 100644 --- a/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs @@ -20,7 +20,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Grouped by what they have in common rather than split across the handlers they touch: every one /// of them is a value leaving the system that should not, and none is visible from reading the -/// handler alone — a leaked credential looks exactly like a populated field, and a missing rate +/// handler alone - a leaked credential looks exactly like a populated field, and a missing rate /// limit looks exactly like a fast API. Tests are the only place that distinction is written down. /// [Collection("Bitween")] @@ -28,7 +28,7 @@ public class PentestFindingTests(BitweenFixture fixture) { /// /// GIG.WEB-003. The list endpoint returned every integration's adapter properties in cleartext - /// — storage keys, an OAuth secret, partner passwords — to anyone allowed to see the list. + /// - storage keys, an OAuth secret, partner passwords - to anyone allowed to see the list. /// [Fact] public async Task Subscription_list_withholds_adapter_properties() @@ -69,7 +69,7 @@ public async Task Subscription_list_withholds_adapter_properties() /// /// Paired with the test above on purpose: the obvious way to stop leaking the properties is to /// stop selecting them, and that would silently break this search instead. Together they say - /// "loaded, used, withheld" — remove either and one of them fails. + /// "loaded, used, withheld" - remove either and one of them fails. /// [Fact] public async Task Searching_inside_property_values_still_finds_the_subscription() @@ -93,7 +93,7 @@ public async Task Searching_inside_property_values_still_finds_the_subscription( await db.SaveChangesAsync(); var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider); - // Rule is immaterial — this field is filtered in memory by Contains, not translated. + // Rule is immaterial - this field is filtered in memory by Contains, not translated. var response = (SearchyResponse)await handler.Handle( new SearchyRequest("filter=rawsubscriptionproperties:1:findme.example.test") { PageSize = 500 }); @@ -165,7 +165,7 @@ public void Setting_a_password_clears_the_requirement() /// Changing the password ends the sessions opened with the old one. /// /// - /// Signing in with a refresh token skips password verification — it looks the account up by id + /// Signing in with a refresh token skips password verification - it looks the account up by id /// and issues a token from whatever state it is now in. Without this, changing a password that /// had leaked would not remove whoever leaked it: their refresh token would still be exchanged /// for a fresh token, and for the seeded administrator that token would come back with the @@ -199,7 +199,7 @@ await handler.Handle(new ChangePasswordModel /// /// GIG.WEB-006. The sign-in page reads this endpoint before anyone authenticates, and the - /// default GitHub link in it is what led the testers to our public repository — and from there + /// default GitHub link in it is what led the testers to our public repository - and from there /// to the sample signing key and the default administrator password. /// [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/PermissionGuardTests.cs b/SW.Bitween.IntegrationTests/Tests/PermissionGuardTests.cs index 4ee112e5..80f3a9fb 100644 --- a/SW.Bitween.IntegrationTests/Tests/PermissionGuardTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PermissionGuardTests.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// /// Two properties are load-bearing and neither is obvious from reading a handler. First, the -/// built-in roles don't store their grants — Role.SystemPermissions derives them at runtime, +/// built-in roles don't store their grants - Role.SystemPermissions derives them at runtime, /// so a Viewer's exact reach is a computed thing that could quietly change. Second, permissions are /// resolved from the database on every call instead of being carried in the token, which is the /// only reason revoking a role can take effect before the token expires. That second one is a @@ -105,7 +105,7 @@ public async Task Revoking_a_role_takes_effect_without_a_new_token() await ctx.EnsurePermission(db, Permissions.Subscriptions.Create); - // Same RequestContext, same claims — only the database changed. If grants were baked into + // Same RequestContext, same claims - only the database changed. If grants were baked into // the token this would keep passing until it expired. var link = db.Set().Single(l => l.AccountId == account.Id && l.RoleId == Role.MemberId); db.Set().Remove(link); diff --git a/SW.Bitween.IntegrationTests/Tests/PipelineEndToEndTests.cs b/SW.Bitween.IntegrationTests/Tests/PipelineEndToEndTests.cs index 7b79883f..c0624ade 100644 --- a/SW.Bitween.IntegrationTests/Tests/PipelineEndToEndTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PipelineEndToEndTests.cs @@ -20,7 +20,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// The whole pipeline, from where a message arrives to the XchangeResult it produces — with +/// The whole pipeline, from where a message arrives to the XchangeResult it produces - with /// subscriptions that actually run serverless adapters rather than stopping at the Xchange row. /// /// The claim under test is the one the external-broker design rests on: past the point of @@ -97,7 +97,7 @@ public async Task The_internal_bus_produces_the_same_outcome_as_an_external_brok ExternalResult = await DriveToResultAsync(externalParent!, external.SubscriptionId); } - // Internal: no data source, no adapter, no broker — the gateway alone. + // Internal: no data source, no adapter, no broker - the gateway alone. var internalSetup = await ArrangeAsync(endpoint: null, EchoHandler); await SubmitInternallyAsync(internalSetup.DocumentId, payload); @@ -119,7 +119,7 @@ public async Task The_internal_bus_produces_the_same_outcome_as_an_external_brok /// /// One gateway, two routes, different filters. The message must run the subscription whose - /// filter matches and only that one — routing is the gateway's job whatever fed it. + /// filter matches and only that one - routing is the gateway's job whatever fed it. /// [Fact] public async Task A_filter_on_a_route_selects_which_subscription_runs() @@ -191,7 +191,7 @@ public async Task A_resident_adapter_and_classic_adapters_serve_one_message_toge /// /// A handler that fails must NOT unwind the ingest. The message was persisted and acknowledged /// long before the handler ran, so the failure belongs to the XchangeResult and the retry - /// policy — not to the broker, which has already been told the message was taken. + /// policy - not to the broker, which has already been told the message was taken. /// [Fact] public async Task A_failing_handler_is_recorded_as_a_result_not_as_an_ingest_failure() @@ -230,7 +230,7 @@ private record Setup(int DocumentId, int SubscriptionId, int GatewayId, int Data /// /// Document, subscription with adapters, gateway, and one route. When - /// is null the gateway is an INTERNAL one — no data source, which is what null has always meant. + /// is null the gateway is an INTERNAL one - no data source, which is what null has always meant. /// private async Task ArrangeAsync(string? endpoint, string handlerId, IDictionary? handlerProperties = null, @@ -297,7 +297,7 @@ private async Task ArrangeAsync(string? endpoint, string handlerId, await db.SaveChangesAsync(); // Ten-minute singleton snapshot: without this the pipeline reads configuration from - // before this test's own setup — see GatewayRoutingTests. + // before this test's own setup - see GatewayRoutingTests. scope.ServiceProvider.GetRequiredService().Revoke(); return new Setup(document.Id, subscription.Id, gateway.Id, dataSourceId); @@ -363,7 +363,7 @@ private Task ProcessAsync(string xchangeId) => Run(scope => /// /// A gateway hit produces an Xchange carrying a SubscriptionId; there is no explicit parent - /// link on the entity, so these are found by Document — unique per test, so unambiguous. + /// link on the entity, so these are found by Document - unique per test, so unambiguous. /// private async Task> ChildXchangesAsync(int documentId) { diff --git a/SW.Bitween.IntegrationTests/Tests/PostgreSqlAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/PostgreSqlAdapterTests.cs index 6aa9c91c..f5cbe72a 100644 --- a/SW.Bitween.IntegrationTests/Tests/PostgreSqlAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PostgreSqlAdapterTests.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// The PostgreSQL data source provider, against a real PostgreSQL. /// /// The same suite as Oracle's, which is the point: both engines run on one core, so the parts that -/// are shared should behave identically and the parts that differ should differ *visibly* — in the +/// are shared should behave identically and the parts that differ should differ *visibly* - in the /// capability list, and in how a routine returns rows. Everything here goes over the resident /// transport into a real adapter process and out to a real database. /// @@ -76,14 +76,14 @@ public async Task Connection_test_reports_each_stage_and_prepares_every_statemen // Npgsql refuses to prepare a statement whose parameters have not been supplied, so the // core declares an empty placeholder for each before preparing. Without that, every - // parameterised statement would fail this check — which is why one is named here. + // parameterised statement would fail this check - which is why one is named here. Assert.Contains("statement:ordersForCustomer", steps); Assert.All(result["steps"]!, s => Assert.True(s.Value("ok"), s.ToString())); } /// /// Where the two engines are honestly different. PostgreSQL has no REF CURSOR, so a CALL cannot - /// hand back rows — declared false rather than glossed over — but it does have arrays and + /// hand back rows - declared false rather than glossed over - but it does have arrays and /// repeatable-read, which Oracle's list does not claim. /// [SkippableFact] @@ -159,7 +159,7 @@ public async Task Discover_lists_sequences_with_their_current_value() /// /// Functions and procedures are listed separately, because on PostgreSQL they are genuinely - /// different things — one is queried, the other is called — and lumping them together is how a + /// different things - one is queried, the other is called - and lumping them together is how a /// caller ends up using the wrong verb. /// [SkippableFact] @@ -291,7 +291,7 @@ public async Task A_write_with_returning_hands_back_the_row_it_wrote() } /// - /// The PostgreSQL substitute for Oracle's REF CURSOR. Same outcome — rows from a routine — but + /// The PostgreSQL substitute for Oracle's REF CURSOR. Same outcome - rows from a routine - but /// reached with Query rather than Call, which is what `procedureResultSets: false` is telling /// whoever reads the capability list. /// @@ -413,7 +413,7 @@ public async Task Two_subscriptions_on_one_data_source_do_not_share_a_cursor() Assert.NotEmpty(forFirst); - // Subscription 202 has never read anything, so it must see the same rows from the start — + // Subscription 202 has never read anything, so it must see the same rows from the start - // not the ones left over after 101 moved the cursor. await adapter.InvokeAsync("Initialize", timeoutSeconds: 60, properties: second); var forSecond = await adapter.InvokeAsync>("ListFiles", timeoutSeconds: 60, @@ -426,7 +426,7 @@ public async Task Two_subscriptions_on_one_data_source_do_not_share_a_cursor() // And each was saved under its own name, rather than one overwriting the other. Assert.NotNull(await CursorAsync("receive.cursor.101")); - // 202 listed but accepted nothing, so it has no cursor yet — which is the point: its + // 202 listed but accepted nothing, so it has no cursor yet - which is the point: its // progress is its own, and reading did not move it. Assert.Null(await CursorAsync("receive.cursor.202")); } @@ -472,7 +472,7 @@ await store.SetAsync(new AdapterStateKey } /// - /// One connection, two receivers, different statements — the case the settings could not + /// One connection, two receivers, different statements - the case the settings could not /// express while they lived on the data source. /// /// What each reads and how comes from the invocation: the statement by name, the mode and @@ -534,7 +534,7 @@ public async Task A_receive_statement_that_is_not_configured_is_refused() // ---------------------------------------------------------------- validating /// - /// A statement is checked by PREPARING it — parsed and planned by the engine, never run — so + /// A statement is checked by PREPARING it - parsed and planned by the engine, never run - so /// a typo is refused where it was made instead of surfacing later as a failed connection test /// or a failed message. /// @@ -585,7 +585,7 @@ public async Task The_wrong_placeholder_prefix_is_named_rather_than_left_to_a_co /// /// A statement meant to be CALLED holds a procedure name, not SQL. Preparing it as text is a - /// syntax error every time, so it is accepted with a note saying what was not checked — + /// syntax error every time, so it is accepted with a note saying what was not checked - /// refusing it would block a statement that is perfectly correct. /// [SkippableFact] @@ -635,7 +635,7 @@ await _fixture.App.Services.GetRequiredService() }, default); /// - /// The mark-processed statement — Camel's onConsume — running per row once Bitween has accepted + /// The mark-processed statement - Camel's onConsume - running per row once Bitween has accepted /// it. Proven by reading the flag back through the adapter, not by trusting that it ran. /// [SkippableFact] @@ -692,7 +692,7 @@ async Task CreateDataSourceAsync() ["MinPoolSize"] = "1", ["MaxPoolSize"] = "5", - // Note @name, not :name — the colon collides with PostgreSQL's :: cast operator. + // Note @name, not :name - the colon collides with PostgreSQL's :: cast operator. ["Statements"] = $@"{{ ""seededOrders"": ""select * from {PostgreSqlDbFixture.Table} where id <= 25 order by id"", ""ordersForCustomer"": ""select * from {PostgreSqlDbFixture.Table} where customer = @customer order by id"", diff --git a/SW.Bitween.IntegrationTests/Tests/PrivateAzureContainerTests.cs b/SW.Bitween.IntegrationTests/Tests/PrivateAzureContainerTests.cs index e3f34b83..f53a49ee 100644 --- a/SW.Bitween.IntegrationTests/Tests/PrivateAzureContainerTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/PrivateAzureContainerTests.cs @@ -14,8 +14,8 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// The startup step that keeps Bitween's container private on Azure, against Azurite — the Azure storage -/// emulator — with the container set up the way the storage library sets it up: open to everyone. +/// The startup step that keeps Bitween's container private on Azure, against Azurite - the Azure storage +/// emulator - with the container set up the way the storage library sets it up: open to everyone. /// public sealed class PrivateAzureContainerTests : IAsyncLifetime { @@ -70,7 +70,7 @@ public async Task When_it_isnt_allowed_the_reason_is_kept_for_the_settings_page( await Container("refused-container").CreateAsync(PublicAccessType.BlobContainer); var access = new StorageAccess(null!, new BitweenOptions(), NullLogger.Instance); - // Signed with the wrong key, Azure refuses — as it does a login without the right role. + // Signed with the wrong key, Azure refuses - as it does a login without the right role. var wrongKey = Convert.ToBase64String(new byte[64]); await Run(new PrivateAzureContainer(Container("refused-container", wrongKey), access, NullLogger.Instance)); diff --git a/SW.Bitween.IntegrationTests/Tests/QuartzClusteringTests.cs b/SW.Bitween.IntegrationTests/Tests/QuartzClusteringTests.cs new file mode 100644 index 00000000..a8b2e931 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/QuartzClusteringTests.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Npgsql; +using Quartz; +using SW.PrimitiveTypes; +using SW.Scheduler; +using SW.Scheduler.EfCore; +using SW.Scheduler.PgSql; +using Testcontainers.PostgreSql; +using Xunit; +using Xunit.Abstractions; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Whether a scheduled job runs once per fire or once per replica, answered with two real Quartz +/// schedulers registered the way Startup registers them, sharing one database. +/// +/// The question matters because the docs and several comments said Quartz was not clustered, so +/// every replica ran every job. SW.Scheduler's persistent stores turn clustering on unconditionally +/// (8.1.7 already did, and 10.0.0 is pinned now): each node gets its own instance id and fires are +/// claimed through the job store's row locks. A lease on top would not remove duplicates that do not +/// exist; it would skip the fires that the other node happened to claim. +/// +public sealed class QuartzClusteringTests(ITestOutputHelper output) : IAsyncLifetime +{ + // Quartz keeps the logger factory of the last host started in a static, so a factory disposed + // with one test's hosts would break the next test's. One that is never disposed avoids that. + private static readonly ILoggerFactory Logging = LoggerFactory.Create(_ => { }); + + private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build(); + private readonly ProbeRuns _runs = new(); + + public async Task InitializeAsync() + { + await _postgres.StartAsync(); + await using var db = new PgSql.BitweenDbContext( + Configure(new DbContextOptionsBuilder()).Options, + new RequestContext(), new NoPublish()); + await db.Database.MigrateAsync(); + } + + public async Task DisposeAsync() => await _postgres.DisposeAsync(); + + /// + /// What the rest rests on. A scheduler package that stopped clustering would fail here first. + /// + [Fact] + public async Task The_scheduler_as_Bitween_registers_it_is_clustered() + { + using var a = await StartNodeAsync("a"); + using var b = await StartNodeAsync("b"); + + var metaA = await (await a.Services.GetRequiredService().GetScheduler()).GetMetaData(); + var metaB = await (await b.Services.GetRequiredService().GetScheduler()).GetMetaData(); + + Assert.True(metaA.JobStoreClustered); + Assert.True(metaB.JobStoreClustered); + Assert.NotEqual(metaA.SchedulerInstanceId, metaB.SchedulerInstanceId); + + await a.StopAsync(); + await b.StopAsync(); + } + + [Fact] + public async Task Two_nodes_never_run_the_same_fire() + { + using var a = await StartNodeAsync("a"); + using var b = await StartNodeAsync("b"); + await ScheduleEverySecondAsync(a); + + await Task.Delay(TimeSpan.FromSeconds(10)); + await a.StopAsync(); + await b.StopAsync(); + + Assert.True(_runs.All.Count >= 5, $"only {_runs.All.Count} run(s) in ten seconds"); + Assert.True(_runs.RunTwice().Count == 0, Describe()); + } + + /// + /// Nothing ties a job to the node that last ran it. A node that finds nothing to claim sleeps + /// for Quartz's idle wait (30 s) before looking again, so that is how long a handover can take; + /// a node that dies rather than stops also has its claimed fires recovered once it misses its + /// check-ins (10 s interval, 20 s threshold). + /// + [Fact] + public async Task When_the_node_running_a_job_stops_the_other_carries_on() + { + using var a = await StartNodeAsync("a"); + using var b = await StartNodeAsync("b"); + await ScheduleEverySecondAsync(a); + + await WaitUntilAsync(() => _runs.All.Count >= 3); + var busiest = _runs.All.GroupBy(r => r.Node).OrderByDescending(g => g.Count()).First().Key; + var (stopped, survivor) = busiest == "a" ? (a, "b") : (b, "a"); + + await stopped.StopAsync(); + var stoppedAt = DateTime.UtcNow; + await WaitUntilAsync(() => _runs.All.Count(r => r.Node == survivor && r.At > stoppedAt) >= 3, + TimeSpan.FromSeconds(45)); + await (survivor == "a" ? a : b).StopAsync(); + + var tookOver = _runs.All.Where(r => r.Node == survivor && r.At > stoppedAt).Min(r => r.At); + output.WriteLine($"{survivor} took over {(tookOver - stoppedAt).TotalSeconds:0.0} s after {busiest} stopped."); + Assert.True(_runs.RunTwice().Count == 0, Describe()); + } + + [Fact] + public async Task A_single_node_runs_every_fire() + { + using var only = await StartNodeAsync("only"); + await ScheduleEverySecondAsync(only); + + await WaitUntilAsync(() => _runs.All.Count >= 5); + await only.StopAsync(); + + Assert.All(_runs.All, r => Assert.Equal("only", r.Node)); + } + + // ---------------------------------------------------------------- helpers + + private string Describe() => + string.Join("; ", _runs.RunTwice()); + + private async Task StartNodeAsync(string name) + { + var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton(Logging); + services.AddScoped(); + services.AddSingleton(); + services.AddDbContext(c => Configure(c)); + + // As Startup registers it for PostgreSQL, scanning only this assembly for jobs. + services.AddPgSqlScheduler(pg => + { + pg.ConnectionString = _postgres.GetConnectionString(); + pg.Schema = PgSql.BitweenDbContext.Schema; + }, + assemblies: new[] { typeof(ClusterProbeJob).Assembly }); + services.AddSchedulerMonitoring(); + }) + .Build(); + + await host.StartAsync(); + var scheduler = await host.Services.GetRequiredService().GetScheduler(); + scheduler.ListenerManager.AddJobListener(new ProbeListener(name, _runs)); + return host; + } + + /// Retried because the job is registered by a hosted service that may not have run yet. + private static async Task ScheduleEverySecondAsync(IHost node) + { + await using var scope = node.Services.CreateAsyncScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + + var deadline = DateTime.UtcNow.AddSeconds(30); + while (true) + { + try + { + await repo.Schedule("* * * * * ?"); + return; + } + catch when (DateTime.UtcNow < deadline) + { + await Task.Delay(500); + } + } + } + + private static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(30)); + while (!condition() && DateTime.UtcNow < deadline) await Task.Delay(200); + Assert.True(condition(), "the job did not run in time"); + } + + private T Configure(T builder) where T : DbContextOptionsBuilder + { + var dataSource = new NpgsqlDataSourceBuilder(_postgres.GetConnectionString()); + dataSource.EnableDynamicJson(); + builder.UseSnakeCaseNamingConvention().UseNpgsql(dataSource.Build(), b => + { + b.MigrationsHistoryTable("_ef_migrations_history", PgSql.BitweenDbContext.Schema); + b.MigrationsAssembly(typeof(PgSql.DbType).Assembly.FullName); + }); + return builder; + } +} + +/// One fire: the time it was due, which node ran it and when. +public sealed record ProbeRun(string Node, DateTime Due, DateTime At); + +public sealed class ProbeRuns +{ + private readonly ConcurrentQueue _runs = new(); + + public void Add(ProbeRun run) => _runs.Enqueue(run); + + public IReadOnlyList All => _runs.ToList(); + + /// Fires that more than one run answered, by the time each was due. + public IReadOnlyList RunTwice() => All + .GroupBy(r => r.Due) + .Where(g => g.Count() > 1) + .Select(g => $"{g.Key:HH:mm:ss.fff} ran on {string.Join(", ", g.Select(r => r.Node))}") + .ToList(); +} + +/// +/// Records each run of with the fire it answered, which the job +/// itself is not told. +/// +public sealed class ProbeListener(string node, ProbeRuns runs) : IJobListener +{ + public string Name => $"probe-{node}"; + + public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default) + { + if (context.JobDetail.Key.Group.Contains(nameof(ClusterProbeJob))) + runs.Add(new ProbeRun(node, + context.ScheduledFireTimeUtc?.UtcDateTime ?? context.FireTimeUtc.UtcDateTime, DateTime.UtcNow)); + return Task.CompletedTask; + } + + public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default) => + Task.CompletedTask; + + public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, + CancellationToken cancellationToken = default) => Task.CompletedTask; +} + +/// Configured as Bitween's own jobs are. +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public sealed class ClusterProbeJob : IScheduledJob +{ + public Task Execute() => Task.CompletedTask; +} + +/// The DbContext takes a publisher for its outbox; nothing here publishes. +public sealed class NoPublish : IPublish +{ + public Task Publish(TMessage message) => Task.CompletedTask; + public Task Publish(string messageTypeName, string message) => Task.CompletedTask; + public Task Publish(string messageTypeName, byte[] message) => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Tests/QuietSubscriptionsTests.cs b/SW.Bitween.IntegrationTests/Tests/QuietSubscriptionsTests.cs new file mode 100644 index 00000000..f09b47ca --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/QuietSubscriptionsTests.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Adapters; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// A subscription that expects traffic and gets none: nothing fails, so without QuietSubscriptionsJob +/// nobody hears of it. The job runs here at the real time, against exchanges made by XchangeService. +/// +[Collection("Bitween")] +public class QuietSubscriptionsTests(BitweenFixture fixture) +{ + static readonly string[] DayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + // Every day, all day, UTC: the window is always open, so only the gap decides. + static ExpectTraffic Always(int every = 5) => new() { EveryMinutes = every }; + + [Fact] + public async Task Silence_inside_the_window_is_raised_once_and_tells_the_notifier() + { + var (id, name, token) = await Watched(Always(15)); + + await RunJob(); + await RunJob(); + + var quietSince = await QuietSince(id); + Assert.NotNull(quietSince); + var sent = Assert.Single(Received(token)); + Assert.Equal("gone-quiet", sent.Event); + Assert.False(sent.Success); + Assert.Equal(id, sent.SubscriptionId); + Assert.Equal(name, sent.SubscriptionName); + Assert.Equal("Quiet doc " + token, sent.DocumentName); + Assert.Equal($"No exchanges in the last 7 days; {name} expects at least one every 15 minutes, every day UTC.", + sent.Exception); + Assert.False(string.IsNullOrEmpty(sent.CorrelationId)); + + // Logged, not recorded against an exchange: there is none. + await using var scope = fixture.CreateScope(); + Assert.False(await scope.ServiceProvider.GetRequiredService() + .Set().AnyAsync(n => n.XchangeId == sent.Id)); + } + + [Fact] + public async Task An_exchange_inside_the_gap_keeps_it_from_going_quiet() + { + var (id, _, token) = await Watched(Always(15)); + await Exchange(id); + + await RunJob(); + + Assert.Null(await QuietSince(id)); + Assert.Empty(Received(token)); + } + + [Fact] + public async Task The_last_exchange_is_named_when_it_goes_quiet() + { + var (id, name, token) = await Watched(Always()); + var last = await Exchange(id); + + // Ten minutes on, the five-minute gap has passed. + await RunJob(DateTime.UtcNow.AddMinutes(10)); + + var sent = Assert.Single(Received(token)); + Assert.Equal($"No exchanges since {last:yyyy-MM-dd HH:mm} UTC; {name} expects at least one every 5 minutes, every day UTC.", + sent.Exception); + } + + [Fact] + public async Task Outside_the_window_silence_is_not_raised() + { + // Only on the day after tomorrow, so never today. + var (id, _, token) = await Watched(new ExpectTraffic + { EveryMinutes = 5, Days = [DayNames[((int)DateTime.UtcNow.DayOfWeek + 2) % 7]] }); + + await RunJob(); + + Assert.Null(await QuietSince(id)); + Assert.Empty(Received(token)); + } + + [Fact] + public async Task An_exchange_after_it_went_quiet_clears_it_and_says_it_is_back() + { + var (id, _, token) = await Watched(Always()); + await RunJob(); + Assert.NotNull(await QuietSince(id)); + + await Exchange(id); + await RunJob(); + await RunJob(); + + Assert.Null(await QuietSince(id)); + var sent = Received(token); + Assert.Equal(["gone-quiet", "back"], sent.Select(s => s.Event)); + Assert.True(sent[1].Success); + Assert.Matches(@"^Exchanges arriving again after \d+ minutes?\.$", sent[1].Exception); + } + + [Fact] + public async Task Traffic_coming_back_outside_the_window_still_clears_it() + { + var (id, _, token) = await Watched(Always()); + await RunJob(); + + // The window moves away from now; the flag it raised stays until traffic returns. + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var subscription = await db.Set().SingleAsync(s => s.Id == id); + subscription.SetExpectTraffic(new ExpectTraffic + { EveryMinutes = 5, Days = [DayNames[((int)DateTime.UtcNow.DayOfWeek + 2) % 7]] }); + await db.SaveChangesAsync(); + } + Assert.NotNull(await QuietSince(id)); + + await Exchange(id); + await RunJob(); + + Assert.Null(await QuietSince(id)); + Assert.Equal(["gone-quiet", "back"], Received(token).Select(s => s.Event)); + } + + [Fact] + public async Task A_paused_or_inactive_subscription_is_not_raised() + { + var (paused, _, pausedToken) = await Watched(Always()); + var (inactive, _, inactiveToken) = await Watched(Always()); + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + (await db.Set().SingleAsync(s => s.Id == paused)).Pause(); + (await db.Set().SingleAsync(s => s.Id == inactive)).Inactive = true; + await db.SaveChangesAsync(); + } + + await RunJob(); + + Assert.Null(await QuietSince(paused)); + Assert.Null(await QuietSince(inactive)); + Assert.Empty(Received(pausedToken)); + Assert.Empty(Received(inactiveToken)); + } + + [Fact] + public async Task Only_notifiers_on_failed_results_are_told() + { + var (id, _, token) = await Watched(Always()); + var successOnly = $"success-only-{Guid.NewGuid():N}"; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var notifier = new Notifier($"Quiet success {Guid.NewGuid():N}") + { + HandlerId = nameof(NativeTestResponder), RunOnSuccessfulResult = true, RunOnSubscriptions = [id], + }; + notifier.SetDictionaries(new Dictionary { ["Body"] = successOnly }); + db.Add(notifier); + await db.SaveChangesAsync(); + } + + await RunJob(); + + Assert.Single(Received(token)); + Assert.Empty(Received(successOnly)); + } + + [Fact] + public async Task The_expectation_is_saved_checked_and_read_back_with_quiet_since() + { + var (id, _, _) = await Watched(null); + + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + var get = await Read(scope, id); + get.ExpectTraffic = new ExpectTraffic + { EveryMinutes = 15, Days = ["thu", "Sun", "Mon", "Tue", "Wed"], From = "08:00", To = "22:00", TimeZone = "Asia/Riyadh" }; + await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Handle(id, get); + } + + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + var read = await Read(scope, id); + Assert.Equal(["Sun", "Mon", "Tue", "Wed", "Thu"], read.ExpectTraffic!.Days); + Assert.Equal("Asia/Riyadh", read.ExpectTraffic.TimeZone); + Assert.Null(read.QuietSince); + + read.ExpectTraffic.TimeZone = "Mars/Olympus"; + var refused = await Assert.ThrowsAsync(() => ActivatorUtilities + .CreateInstance(scope.ServiceProvider).Handle(id, read)); + Assert.Contains("isn't a time zone", refused.Message); + } + + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + var read = await Read(scope, id); + read.ExpectTraffic!.EveryMinutes = 4; + var refused = await Assert.ThrowsAsync(() => ActivatorUtilities + .CreateInstance(scope.ServiceProvider).Handle(id, read)); + Assert.Contains("5 to", refused.Message); + } + } + + [Fact] + public async Task Removing_the_expectation_clears_quiet_since() + { + var (id, _, _) = await Watched(Always()); + await RunJob(); + + await using (var scope = fixture.CreateScope()) + { + scope.Superuser(); + var get = await Read(scope, id); + Assert.NotNull(get.QuietSince); + get.ExpectTraffic = null; + await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Handle(id, get); + } + + Assert.Null(await QuietSince(id)); + } + + static async Task Read(AsyncServiceScope scope, int id) => + (SubscriptionGet)await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Handle(id); + + /// An active subscription expecting , and a notifier on its failures. + async Task<(int Id, string Name, string Token)> Watched(ExpectTraffic expect) + { + var token = $"quiet-{Guid.NewGuid():N}"; + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var document = new Document(null, "Quiet doc " + token, DocumentFormat.Json); + db.Add(document); + await db.SaveChangesAsync(); + // A gateway type: a Receiving one would need schedules before an update is accepted. + var subscription = new Subscription($"Orders {token}", document.Id, SubscriptionType.GatewayApiCall) { Inactive = false }; + subscription.SetExpectTraffic(expect); + db.Add(subscription); + await db.SaveChangesAsync(); + + var notifier = new Notifier($"Quiet {token}") + { + HandlerId = nameof(NativeTestResponder), RunOnFailedResult = true, RunOnSubscriptions = [subscription.Id], + }; + notifier.SetDictionaries(new Dictionary { ["Body"] = token }); + db.Add(notifier); + await db.SaveChangesAsync(); + scope.ServiceProvider.GetRequiredService().Revoke(); + return (subscription.Id, subscription.Name, token); + } + + async Task Exchange(int subscriptionId) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscription = await db.Set().SingleAsync(s => s.Id == subscriptionId); + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(subscription, new XchangeFile("{}")); + await db.SaveChangesAsync(); + return xchange.StartedOn; + } + + async Task RunJob(DateTime? at = null) + { + await using var scope = fixture.CreateScope(); + await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Run(at ?? DateTime.UtcNow); + } + + async Task QuietSince(int id) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService().Set().AsNoTracking() + .Where(s => s.Id == id).Select(s => s.QuietSince).SingleAsync(); + } + + static List Received(string token) => + NativeTestResponder.Received.TryGetValue(token, out var sent) + ? sent.Select(JsonConvert.DeserializeObject).ToList()! + : []; +} diff --git a/SW.Bitween.IntegrationTests/Tests/RabbitBusAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/RabbitBusAdapterTests.cs index 32fc1d22..3dbf35ca 100644 --- a/SW.Bitween.IntegrationTests/Tests/RabbitBusAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RabbitBusAdapterTests.cs @@ -22,7 +22,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// conditions that demonstrate one. /// /// The gateway tests prove a message arrives. These ask the harder questions: what happens when two -/// messages are byte-for-byte identical, and what happens when many arrive at once — the two cases +/// messages are byte-for-byte identical, and what happens when many arrive at once - the two cases /// where a consumer quietly loses data instead of failing loudly. /// [Collection("Bitween")] @@ -33,7 +33,7 @@ public class RabbitBusAdapterTests(BitweenFixture fixture) /// /// Deduplication needs a key, and a publisher that sets no MessageId gives us none. Hashing the /// body to manufacture one looks like a reasonable fallback and is not: a customer sending the - /// same instruction twice — reorder the same item, the same daily totals, a heartbeat — would + /// same instruction twice - reorder the same item, the same daily totals, a heartbeat - would /// have the second silently swallowed for the whole deduplication window, which is thirty days /// by default. No error, no Xchange, no way to notice. /// @@ -53,7 +53,7 @@ public async Task Two_identical_messages_with_no_message_id_are_two_Xchanges() PublishRaw(queue, body, messageId: null); await WaitAsync(async () => await CountAsync(documentId) >= 2, TimeSpan.FromSeconds(45), - "the second identical message was swallowed — a content hash is being used as identity"); + "the second identical message was swallowed - a content hash is being used as identity"); Assert.Equal(2, await CountAsync(documentId)); } @@ -90,12 +90,12 @@ await WaitAsync(async () => await CountAsync(documentId) >= 2, TimeSpan.FromSeco /// Concurrent deliveries, which is the normal state of a consumer with prefetch above one. /// /// Every delivery is handled on the thread pool, so acknowledgements land on the shared channel - /// from several threads at once — and RabbitMQ.Client does not support concurrent application + /// from several threads at once - and RabbitMQ.Client does not support concurrent application /// operations on one model. A corrupted frame stream drops the channel and redelivers /// everything in flight, so what this asserts is the outcome that matters: every message /// arrives, exactly once, and the connection is still up afterwards. /// - /// This is an integrity test, not a race detector — a race that is not serialised may still + /// This is an integrity test, not a race detector - a race that is not serialised may still /// pass on a quiet machine. It fails loudly when the ordering breaks, which is what a test can /// honestly offer here. /// @@ -114,7 +114,7 @@ await WaitAsync(async () => await CountAsync(documentId) >= count, TimeSpan.From $"only {await CountAsync(documentId)} of {count} messages became an Xchange"); // Nothing extra either: a channel that dropped mid-flight would redeliver, and with a - // dedupe key per message that would show up as a shortfall rather than a surplus — so + // dedupe key per message that would show up as a shortfall rather than a surplus - so // check both ends. await Task.Delay(TimeSpan.FromSeconds(5)); Assert.Equal(count, await CountAsync(documentId)); diff --git a/SW.Bitween.IntegrationTests/Tests/ReadGuardTests.cs b/SW.Bitween.IntegrationTests/Tests/ReadGuardTests.cs index f3090a27..bf84ff64 100644 --- a/SW.Bitween.IntegrationTests/Tests/ReadGuardTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ReadGuardTests.cs @@ -55,6 +55,7 @@ public async Task One_view_permission_reads_its_own_list_and_no_other() { ["partners"] = Search(scope), ["exchanges"] = Search(scope), + ["exchange summary"] = Search(scope), ["subscriptions"] = Search(scope), ["notifiers"] = Search(scope), ["API gateways"] = Search(scope), diff --git a/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptCleanupTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptCleanupTests.cs new file mode 100644 index 00000000..47acba7c --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptCleanupTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// The nightly clean-up of receive-run history, against the real database: runs older than the +/// retention go, and everything inside it stays. +/// +[Collection("Bitween")] +public class ReceiveAttemptCleanupTests(BitweenFixture fixture) +{ + [Fact] + public async Task Runs_past_the_retention_are_deleted_and_recent_ones_kept() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService(); + var now = DateTime.UtcNow; + + ReceiveAttempt Run(DateTime startedOn) => new() + { + SubscriptionId = 1, StartedOn = startedOn, FinishedOn = startedOn.AddSeconds(1), + Outcome = ReceiveOutcome.NoNewData, ExchangeIds = [], + }; + var expired = Run(now.AddDays(-options.ReceiveAttemptRetentionDays - 1)); + var justInside = Run(now.AddDays(-options.ReceiveAttemptRetentionDays).AddHours(1)); + var today = Run(now); + db.AddRange(expired, justInside, today); + await db.SaveChangesAsync(); + + await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Execute(); + + var left = await db.Set().AsNoTracking() + .Where(a => a.Id == expired.Id || a.Id == justInside.Id || a.Id == today.Id) + .Select(a => a.Id).ToListAsync(); + Assert.DoesNotContain(expired.Id, left); + Assert.Contains(justInside.Id, left); + Assert.Contains(today.Id, left); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptFilterTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptFilterTests.cs index 992794e8..921a4f56 100644 --- a/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptFilterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ReceiveAttemptFilterTests.cs @@ -88,7 +88,7 @@ public async Task A_run_that_created_nothing_matches_nothing_rather_than_everyth var xs = scope.ServiceProvider.GetRequiredService(); // Exchanges exist; this run just is not responsible for any of them. Dropping the filter - // instead of honouring it would return all of them — and bulk retry acts on this selection. + // instead of honouring it would return all of them - and bulk retry acts on this selection. await ARunThatCreated(db, xs, "Attempt filter bystander"); var empty = new ReceiveAttempt diff --git a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs index 0c9a46a4..fa32f823 100644 --- a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs @@ -58,7 +58,7 @@ public async Task Receiving_job_records_one_attempt_with_the_exchanges_it_create var subscription = new Subscription("Receive Attempt Test", document.Id); subscription.ReceiverId = nameof(NativeTestReceiver); subscription.Inactive = false; - // SetSchedules() throws "Invalid schedule" with none configured — a real Receiving + // SetSchedules() throws "Invalid schedule" with none configured - a real Receiving // subscription always has one, so give this test one too. subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); db.Set().Add(subscription); @@ -109,6 +109,43 @@ public async Task Receiving_job_records_a_failed_attempt_when_listing_files_thro Assert.Empty(attempt.ExchangeIds); } + /// + /// One file that can't be read used to end the run - and, left at the source, every run after + /// it, so nothing behind it was ever received. Now it is left and reported, and the rest go through. + /// + [Fact] + public async Task One_bad_file_is_left_and_reported_while_the_rest_are_received() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + + var document = new Document(null, $"Partly failing {Guid.NewGuid():N}", DocumentFormat.Json); + db.Set().Add(document); + await db.SaveChangesAsync(); + var subscription = new Subscription($"Partly failing {Guid.NewGuid():N}", document.Id) + { + ReceiverId = nameof(NativePartlyFailingTestReceiver), + Inactive = false, + }; + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + fixture.App.Services.GetRequiredService().Revoke(); + var finalizedBefore = NativePartlyFailingTestReceiver.Finalized; + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + Assert.Equal(2, await db.Set().CountAsync(x => x.SubscriptionId == subscription.Id)); + Assert.DoesNotContain("corrupt.json", NativePartlyFailingTestReceiver.Deleted); + Assert.True(NativePartlyFailingTestReceiver.Finalized > finalizedBefore); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.Failed, attempt.Outcome); + Assert.Contains("corrupt.json", attempt.ErrorMessage); + Assert.Equal(2, attempt.ExchangeIds.Length); + } + [Fact] public async Task Receiving_job_records_no_new_data_when_nothing_is_found() { @@ -151,7 +188,7 @@ public async Task Receiving_job_does_nothing_for_inactive_subscription() var subscription = new Subscription("Inactive Receiver", document.Id); subscription.ReceiverId = nameof(NativeTestReceiver); - // Inactive = true by default — job should skip it + // Inactive = true by default - job should skip it db.Set().Add(subscription); await db.SaveChangesAsync(); diff --git a/SW.Bitween.IntegrationTests/Tests/ResidentAsPipelineAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/ResidentAsPipelineAdapterTests.cs index c078fbfa..b942c33e 100644 --- a/SW.Bitween.IntegrationTests/Tests/ResidentAsPipelineAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ResidentAsPipelineAdapterTests.cs @@ -12,11 +12,11 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// A resident adapter used where a CLASSIC one is expected — as a subscription's handler, mapper, +/// A resident adapter used where a CLASSIC one is expected - as a subscription's handler, mapper, /// receiver or validator. /// /// The two lifecycles are installed identically: same zip, same cloud key, same metadata bag. What -/// differs is the entry point — Runner.Run speaks the classic stdin/stdout protocol, while +/// differs is the entry point - Runner.Run speaks the classic stdin/stdout protocol, while /// Runner.RunResident dials back over a socket and waits for the resident host. Nothing in the /// adapter catalog or the invoke path looks at which one an adapter uses, so the question these /// tests answer is what an operator actually gets when they pick one from the dropdown. @@ -24,7 +24,7 @@ namespace SW.Bitween.IntegrationTests.Tests; [Collection("Bitween")] public class ResidentAsPipelineAdapterTests(BitweenFixture fixture) { - // The catalog lists by key prefix — infolink6.{handlers|mappers|receivers|validators} — so an + // The catalog lists by key prefix - infolink6.{handlers|mappers|receivers|validators} - so an // adapter has to be installed under one of those to be selectable at all. private const string ResidentHandlerId = "infolink6.handlers.residenttest"; private const string ClassicHandlerId = "infolink6.handlers.classictest"; @@ -61,13 +61,13 @@ public async Task A_resident_adapter_appears_in_the_handler_dropdown() } /// - /// And here is the problem. The dropdown offers it, and asking for its startup values — which - /// is how the UI builds the properties form — goes down the classic path: spawn the process, + /// And here is the problem. The dropdown offers it, and asking for its startup values - which + /// is how the UI builds the properties form - goes down the classic path: spawn the process, /// talk stdin/stdout, wait. A resident adapter is not listening on stdin; it dialled out and is /// waiting for a host that, on this path, never calls. /// /// The UI swallows this and shows an adapter with no properties, so what an operator sees is a - /// selectable adapter that simply has nothing to configure — not a broken one. + /// selectable adapter that simply has nothing to configure - not a broken one. /// [Fact] public async Task Asking_a_resident_adapter_for_its_startup_values_does_not_work() @@ -78,7 +78,7 @@ public async Task Asking_a_resident_adapter_for_its_startup_values_does_not_work var classic = await StartupValuesAsync(ClassicHandlerId); Assert.NotNull(classic); - // The resident one does not — it either fails outright or hands back nothing, and either + // The resident one does not - it either fails outright or hands back nothing, and either // way the properties form is empty. var resident = await Record.ExceptionAsync(() => StartupValuesAsync(ResidentHandlerId)); @@ -95,7 +95,7 @@ public async Task Asking_a_resident_adapter_for_its_startup_values_does_not_work /// An adapter named nothing like the convention still appears, because it SAID what it is. /// /// This is the point of stamping the kind at publish time: the id stops having to carry the - /// classification, so an adapter can be reclassified without being renamed — and a rename is + /// classification, so an adapter can be reclassified without being renamed - and a rename is /// not free, because every subscription stores the id it was configured with. /// [Fact] @@ -130,7 +130,7 @@ public async Task An_adapter_with_no_declared_kind_is_still_found_by_its_name() var listed = await ListAsync("handlers"); - // Installed with no Kind at all — found purely by the infolink6.handlers. prefix. + // Installed with no Kind at all - found purely by the infolink6.handlers. prefix. Assert.Contains(ClassicHandlerId, listed); } diff --git a/SW.Bitween.IntegrationTests/Tests/ResidentPipelineAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/ResidentPipelineAdapterTests.cs index c3410782..017488ab 100644 --- a/SW.Bitween.IntegrationTests/Tests/ResidentPipelineAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ResidentPipelineAdapterTests.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// A RESIDENT adapter used as an ordinary pipeline adapter — a subscription's handler or mapper. +/// A RESIDENT adapter used as an ordinary pipeline adapter - a subscription's handler or mapper. /// /// The two packaged lifecycles are published identically and differ only in their entry point, so /// nothing about the pipeline's contract changes. What used to happen is that the invoke path did @@ -69,7 +69,7 @@ public async Task A_resident_adapter_answers_the_handler_contract() /// The distinction a single call cannot show: the SAME process serves more than one message. /// /// A classic adapter is a new process per invocation and would answer 1 every time, so a count - /// above 1 is the proof that the instance was kept and reused — which is the only reason to + /// above 1 is the proof that the instance was kept and reused - which is the only reason to /// make an adapter resident in the first place. /// [Fact] @@ -95,7 +95,7 @@ public async Task The_same_instance_serves_more_than_one_message() } /// - /// A session holds one instance across several calls, which is what a receiver depends on — + /// A session holds one instance across several calls, which is what a receiver depends on - /// Initialize, the listing and every GetFile have to reach the same place. /// [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/ResponseSubscriptionTests.cs b/SW.Bitween.IntegrationTests/Tests/ResponseSubscriptionTests.cs index 467f7b16..a6ddb11b 100644 --- a/SW.Bitween.IntegrationTests/Tests/ResponseSubscriptionTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ResponseSubscriptionTests.cs @@ -128,7 +128,7 @@ public async Task A_disabled_response_subscription_does_not_fail_the_delivery_th var result = await Deliver(chain); - // It used to throw here — the lookup returns null for a disabled subscription — which + // It used to throw here - the lookup returns null for a disabled subscription - which // recorded a delivery that had succeeded as failed, and its retry delivered it again. Assert.True(result.Success, result.Exception); Assert.Null(result.Exception); diff --git a/SW.Bitween.IntegrationTests/Tests/RetiringMapperTests.cs b/SW.Bitween.IntegrationTests/Tests/RetiringMapperTests.cs index 66e36c4d..726247d9 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetiringMapperTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetiringMapperTests.cs @@ -115,7 +115,7 @@ public async Task The_old_mapper_is_listed_only_while_a_subscription_still_uses_ { await ParkExistingUsers(); - // Nothing on it: it is gone from the picker, and the new mapper is still there — a filter + // Nothing on it: it is gone from the picker, and the new mapper is still there - a filter // that took out more than it should would pass an assertion about the old one alone. var withoutUsers = await ListedMappers(); Assert.DoesNotContain(OldMapper, withoutUsers); diff --git a/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs index f926126d..a085ea7a 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs @@ -16,13 +16,13 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Sends a real "retry budget exhausted" alert through to a local -/// MailHog instance and reads it back over MailHog's own API — the one part of the feature no unit +/// MailHog instance and reads it back over MailHog's own API - the one part of the feature no unit /// test can prove, because it depends on an actual SMTP handshake succeeding. /// /// /// MailHog is started by alongside PostgreSQL and RabbitMQ, so these /// tests run everywhere the rest of the suite does. They used to return early when a local MailHog -/// was missing, which xunit reports as a pass — a green run then said nothing about whether an alert +/// was missing, which xunit reports as a pass - a green run then said nothing about whether an alert /// can actually be delivered. /// [Collection("Bitween")] @@ -34,7 +34,7 @@ public class RetryAlertServiceTests(BitweenFixture fixture) private string MessagesApi => $"{fixture.MailHogApi}/api/v2/messages"; - // Deleting is only exposed on MailHog's v1 API — the v2 route 404s and would silently leave + // Deleting is only exposed on MailHog's v1 API - the v2 route 404s and would silently leave // messages behind, making the assertions depend on leftovers from the previous run. private async Task ClearMailHog() { @@ -73,7 +73,7 @@ public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_s db.Set().Add(doc); await db.SaveChangesAsync(); - // A group whose own alert config points at MailHog directly — the narrowest level, so the + // A group whose own alert config points at MailHog directly - the narrowest level, so the // resolver has nothing to fall through to and the test proves that level specifically. var groupId = Guid.NewGuid(); var policy = new RetryPolicy @@ -124,7 +124,7 @@ public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_s // Reproduces exactly what TryScheduleAutoRetry does: evaluate against the real budget table, // and when it reports exhaustion, raise the event the same way XchangeResult does in - // production. RetryAlertService.Process is then invoked directly rather than over the bus — + // production. RetryAlertService.Process is then invoked directly rather than over the bus - // this suite calls handlers directly throughout (see RetryJobTests, DelayedRetriesTests) // rather than relying on live message transport, which is SW.Bus's own concern, not this // feature's. @@ -132,7 +132,7 @@ public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_s new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)); // Total is 1, so the first message (a different "parcel" failing the same way) spends the - // whole budget and is itself allowed to retry — exhaustion only shows up for the next one. + // whole budget and is itself allowed to retry - exhaustion only shows up for the next one. var firstMessage = await evaluator.Evaluate(XchangeResultType.Error, "System.TimeoutException: contains timeout", 0); Assert.True(firstMessage.ShouldRetry); @@ -171,7 +171,7 @@ public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_s Assert.True(loggedNotification.Success); Assert.Equal(XchangeNotification.RetryBudgetAlertName, loggedNotification.NotifierName); - // Redelivery of the same event must not double-send — same guard the real bus retry path + // Redelivery of the same event must not double-send - same guard the real bus retry path // relies on. Compared against the count after the first send rather than an absolute // number, so a stray message could never make this pass by accident. var totalAfterFirstSend = await MailHogTotal(); @@ -239,7 +239,7 @@ public async Task A_failed_send_does_not_stop_a_later_delivery() .CreateXchange(sub, new XchangeFile("{}")); await db.SaveChangesAsync(); - // Stands in for a first attempt that threw — a dropped connection, a refused relay. Written + // Stands in for a first attempt that threw - a dropped connection, a refused relay. Written // directly because what matters is the row it leaves behind, not how the send failed. db.Add(XchangeNotification.ForRetryBudgetAlert(xchange.Id, "System.Net.Sockets.SocketException: refused")); await db.SaveChangesAsync(); @@ -261,7 +261,7 @@ public async Task A_failed_send_does_not_stop_a_later_delivery() && n.Success)); // And now that one did get through, the guard has to hold: no third row, no second email. - // Both are asserted — a redelivery that wrongly logged another success while sending nothing + // Both are asserted - a redelivery that wrongly logged another success while sending nothing // would otherwise pass here. var rowsAfterDelivery = await db.Set() .CountAsync(n => n.XchangeId == xchange.Id); diff --git a/SW.Bitween.IntegrationTests/Tests/RetryAndNotifierHardeningTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryAndNotifierHardeningTests.cs new file mode 100644 index 00000000..6f96722f --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/RetryAndNotifierHardeningTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Adapters; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class RetryAndNotifierHardeningTests(BitweenFixture fixture) +{ + /// + /// A scheduled retry whose run throws used to be deleted on the spot, so a moment's database + /// trouble lost it for good. It is moved later instead, and dropped only after repeated failures. + /// + [Fact] + public async Task A_retry_whose_run_fails_is_rescheduled_then_dropped_after_repeated_failures() + { + var id = $"failing-{Guid.NewGuid():N}"[..30]; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Add(new DelayedRetry { Id = id, On = DateTime.UtcNow.AddMinutes(-1) }); + await db.SaveChangesAsync(); + } + + await Fail(id); + + await using (var scope = fixture.CreateScope()) + { + var row = await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(r => r.Id == id); + Assert.Equal(1, row.RunFailures); + Assert.True(row.On > DateTime.UtcNow); + } + + for (var i = 1; i < RetryJob.MaxRunFailures; i++) await Fail(id); + + await using var check = fixture.CreateScope(); + Assert.False(await check.ServiceProvider.GetRequiredService() + .Set().AnyAsync(r => r.Id == id)); + } + + private async Task Fail(string id) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var row = await db.Set().AsNoTracking().SingleAsync(r => r.Id == id); + await RetryJob.AfterRunFailure(db, row, new TimeoutException("the database went away"), + Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + + /// + /// An outage used to mean one notification per failure. Within the quiet window only the first + /// is sent; the rest are recorded as held back and counted into the next one that goes out. + /// + [Fact] + public async Task A_burst_of_failures_sends_one_notification_and_counts_the_rest() + { + var token = $"quiet-{Guid.NewGuid():N}"; + var (subscriptionId, notifierId) = await NotifierOnFailures(token); + + var failures = new List(); + for (var i = 0; i < 4; i++) failures.Add(await FailedExchange(subscriptionId)); + + foreach (var id in failures) await ProcessResult(id); + + Assert.Equal(1, NativeTestResponder.Deliveries.GetValueOrDefault(token)); + + await using var scope = fixture.CreateScope(); + var rows = await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking() + .Where(n => n.NotifierId == notifierId).ToListAsync(); + Assert.Equal(1, rows.Count(n => n.Success)); + Assert.Equal(3, rows.Count(n => n.Suppressed)); + } + + /// The result event delivered twice notifies once. + [Fact] + public async Task A_repeat_of_the_result_event_does_not_notify_twice() + { + var token = $"repeat-{Guid.NewGuid():N}"; + var (subscriptionId, _) = await NotifierOnFailures(token); + var id = await FailedExchange(subscriptionId); + + await ProcessResult(id); + await ProcessResult(id); + + Assert.Equal(1, NativeTestResponder.Deliveries.GetValueOrDefault(token)); + } + + private async Task ProcessResult(string xchangeId) + { + await using var scope = fixture.CreateScope(); + await scope.ServiceProvider.GetRequiredService() + .Process("Hardening" + XchangeService.ResultQueueSuffix, JsonConvert.SerializeObject(new { Id = xchangeId })); + } + + private async Task<(int SubscriptionId, int NotifierId)> NotifierOnFailures(string token) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var document = new Document(null, $"Hardening {Guid.NewGuid():N}", DocumentFormat.Json); + db.Add(document); + await db.SaveChangesAsync(); + var subscription = new Subscription($"Hardening {Guid.NewGuid():N}", document.Id) { Inactive = false }; + db.Add(subscription); + await db.SaveChangesAsync(); + + var notifier = new Notifier($"Hardening {Guid.NewGuid():N}") + { + HandlerId = nameof(NativeTestResponder), + RunOnFailedResult = true, + RunOnSubscriptions = [subscription.Id], + }; + notifier.SetDictionaries(new Dictionary { ["Body"] = token }); + db.Add(notifier); + await db.SaveChangesAsync(); + scope.ServiceProvider.GetRequiredService().Revoke(); + return (subscription.Id, notifier.Id); + } + + private async Task FailedExchange(int subscriptionId) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscription = await db.Set().SingleAsync(s => s.Id == subscriptionId); + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(subscription, new XchangeFile("{}")); + await db.SaveChangesAsync(); + db.Add(new XchangeResult(xchange.Id, null, null, exception: "System.Exception: the partner is down")); + await db.SaveChangesAsync(); + return xchange.Id; + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs index a7020a0f..a7077162 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryChainTests.cs @@ -76,7 +76,7 @@ public async Task Retry_refuses_an_exchange_that_has_already_been_retried() Assert.Contains(second.Id, error.Message); - // Still exactly one retry from it — the point of the rule. + // Still exactly one retry from it - the point of the rule. Assert.Equal(1, await db.Set().CountAsync(x => x.RetryFor == xchange.Id)); } @@ -174,7 +174,7 @@ public async Task BulkRetry_retries_a_chain_once_when_two_of_its_attempts_are_se /// /// Every selection in one chain has to resolve to the same end of it. The first version walked /// the tree a level at a time with a set of nodes already seen, which made a selection lagging - /// behind another stop on a node the leader had stepped over — so it was reported as handing + /// behind another stop on a node the leader had stepped over - so it was reported as handing /// over to an attempt that had itself been retried, and retrying that threw ALREADY_RETRIED and /// took the whole selection down. Needs three or more attempts and two selections at different /// depths to show up at all. @@ -241,7 +241,7 @@ public async Task BulkRetry_selects_every_exchange_a_filter_matches_minus_the_ex var xs = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - // Three failures on one subscription — more than a page would show, in miniature. + // Three failures on one subscription - more than a page would show, in miniature. var (sub, first) = await FailedXchange(db, xs, "Bulk Filter Doc"); var second = await xs.CreateXchange(sub, new XchangeFile("{}")); var third = await xs.CreateXchange(sub, new XchangeFile("{}")); @@ -289,7 +289,7 @@ public async Task BulkRetryPreview_describes_the_same_work_without_doing_it() /// /// Retrying is an operator's action, and until this was added the endpoints took anyone's word /// for it: the UI hid the button without a permission, but the API accepted the call from any - /// signed-in account. PermissionGuardTests could not have caught it — those exercise + /// signed-in account. PermissionGuardTests could not have caught it - those exercise /// EnsurePermission itself, and the gap was in never calling it. /// [Fact] @@ -320,7 +320,7 @@ await Assert.ThrowsAsync(() => /// /// The plan has to answer the question actually being asked. Re-resolving properties needs the - /// subscription, so for an exchange that has none the answer differs with the choice — and a + /// subscription, so for an exchange that has none the answer differs with the choice - and a /// plan that promised a retry the retry then skipped would make the confirmation worthless. /// [Fact] @@ -364,7 +364,7 @@ public async Task BulkRetryPreview_answers_for_the_reset_that_was_asked_about() /// /// A status the filter cannot read used to be dropped, which widened the selection to every - /// exchange there is — harmless in a search, but bulk retry runs over whatever this selects. + /// exchange there is - harmless in a search, but bulk retry runs over whatever this selects. /// [Fact] public async Task BulkRetry_refuses_a_filter_carrying_an_unreadable_status() @@ -421,7 +421,7 @@ public async Task RetrySummary_ranks_the_chains_that_keep_failing_and_says_wheth Assert.Equal(4, listed.GetProperty("Attempts").GetInt32()); Assert.DoesNotContain(chains, c => c.GetProperty("Id").GetString() == healedRetry.Id); - // Superseded attempts are not listed either — only where each chain got to. + // Superseded attempts are not listed either - only where each chain got to. foreach (var stale in new[] { deep.Id, second.Id, third.Id }) Assert.DoesNotContain(chains, c => c.GetProperty("Id").GetString() == stale); @@ -460,7 +460,7 @@ public async Task RetryTree_returns_the_whole_chain_whichever_attempt_is_asked_a /// /// A chain is one piece of work, however many attempts it took. Asking for the newest attempt - /// only is what turns a list of failures into a list of things still to deal with — and, used + /// only is what turns a list of failures into a list of things still to deal with - and, used /// with a bulk retry, means the selection is already the attempts that can be retried. /// [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs index ef865d9b..5c1f1bf1 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs @@ -197,7 +197,7 @@ public async Task BulkRetry_handles_an_exchange_with_no_subscription() var healthy = await xs.CreateXchange(doc, WorkGroup.None, new XchangeFile("{}")); await db.SaveChangesAsync(); - // One selection containing both. This threw before, so the whole bulk retry failed — including + // One selection containing both. This threw before, so the whole bulk retry failed - including // for the exchanges that were perfectly retryable. await new Resources.Xchanges.BulkRetry(db, scope.Superuser(), xs).Handle(new XchangeBulkRetry { diff --git a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs index acaeb1fe..63f741eb 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs @@ -369,7 +369,7 @@ public async Task Group_total_is_shared_across_separate_messages_of_the_same_int var policy = new CustomRetryPolicy { Groups = [group] }; // Reproduces the reported bug: four failing messages, each retried up to its own - // per-message cap of 3, under a shared total of 10 — 12 retries before the fix. + // per-message cap of 3, under a shared total of 10 - 12 retries before the fix. var allowed = 0; for (var message = 0; message < 4; message++) for (var attempt = 0; attempt < 3; attempt++) @@ -513,8 +513,8 @@ public async Task Usage_reports_spent_budget_and_reset_clears_it() GroupId = groupId }); - // The pair keeps its row — every subscription-and-group pair gets one so an alert override - // stays configurable before the first failure — but with nothing spent against the ceiling. + // The pair keeps its row - every subscription-and-group pair gets one so an alert override + // stays configurable before the first failure - but with nothing spent against the ceiling. var afterReset = Assert.Single( (List)await new Usage(db, ctx, Report(scope)).Handle(policyId, new RetryPolicyUsageRequest())); Assert.Equal(0, afterReset.AttemptsUsed); @@ -562,8 +562,8 @@ public async Task Usage_lists_never_failed_pairs_and_skips_groups_that_cannot_ex var rows = (List)await new Usage(db, ctx, Report(scope)) .Handle(policyId, new RetryPolicyUsageRequest()); - // One row, not two: the pair is reported even though nothing has ever failed — otherwise its - // alert override would be unreachable until after the first failure — while the Block group + // One row, not two: the pair is reported even though nothing has ever failed - otherwise its + // alert override would be unreachable until after the first failure - while the Block group // is left out entirely. var row = Assert.Single(rows); Assert.Equal("Timeout", row.GroupName); @@ -604,7 +604,7 @@ public async Task Reset_does_not_touch_counters_of_another_policy() await new ResetUsage(db, ctx).Handle(mineId, new RetryPolicyResetUsage()); // A row now exists for every pair whether or not it has failed, so assert the spent counter - // itself survived — row count alone would pass even if the reset had wrongly cleared it. + // itself survived - row count alone would pass even if the reset had wrongly cleared it. var otherRow = Assert.Single( (List)await new Usage(db, ctx, Report(scope)).Handle(otherId, new RetryPolicyUsageRequest())); Assert.Equal(1, otherRow.AttemptsUsed); @@ -687,7 +687,7 @@ public async Task Attempts_lists_only_this_pairs_stamped_failures_pending_first( var unstamped = new Xchange(sub, new XchangeFile("{}")); var unstampedResult = new XchangeResult(unstamped.Id, null, null, exception: "older timeout"); - // Same policy and same group, different subscription — a row of its own, not this one's. + // Same policy and same group, different subscription - a row of its own, not this one's. var otherPair = new Xchange(otherSub, new XchangeFile("{}")); var otherPairResult = new XchangeResult(otherPair.Id, null, null, exception: "someone else's timeout"); otherPairResult.SetRetryEvaluation(groupId, 0); @@ -839,7 +839,7 @@ public async Task Exhausting_a_budget_claims_the_alert_exactly_once() var groupId = Guid.NewGuid(); var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); - // Spending the budget never alerts — nothing has been refused yet. + // Spending the budget never alerts - nothing has been refused yet. for (var i = 0; i < 3; i++) { var spending = await budget.TryConsume(groupId, 3); @@ -996,7 +996,7 @@ public async Task An_inline_policy_budget_can_be_reported_and_reset_by_subscript Assert.Equal(10, row.AttemptsUsed); Assert.True(row.Exhausted); - // An inline policy has no row to hold a policy-level alert, so nothing resolves from there — + // An inline policy has no row to hold a policy-level alert, so nothing resolves from there - // and that has to read as "nothing configured" rather than as a level being consulted. Assert.Null(row.ResolvedHandlerId); Assert.Null(row.ResolvedFrom); @@ -1041,7 +1041,7 @@ public async Task Allow_without_a_budget_is_rejected_on_save() ] }; - // Nothing to work from — no caps, no delay — so the evaluator could only ever refuse it, and + // Nothing to work from - no caps, no delay - so the evaluator could only ever refuse it, and // refusing quietly reads as retries being broken. Rejected where it is configured instead. await Assert.ThrowsAsync( () => new Create(db, ctx).Handle(PolicyWithBudgetlessGroup("Budgetless Allow", RetryAction.Allow))); @@ -1135,7 +1135,7 @@ public async Task Overriding_an_inherited_alert_keeps_the_password_it_was_only_s Assert.Equal(Sentinel, row.ResolvedHandlerProperties["Password"]); // The page offers "start from what this currently sends", so the masked value is what comes - // back — and there is no override row yet to restore it from. It has to be recovered from the + // back - and there is no override row yet to restore it from. It has to be recovered from the // level the caller was shown it at, or the new override would send with no password at all. await new SaveAlertOverride(db, ctx).Handle(policyId, new RetryAlertOverrideSave { @@ -1163,7 +1163,7 @@ public async Task A_mail_alert_with_a_password_and_no_encryption_is_rejected_on_ model.AlertHandlerId = "NativeSmtpHandler"; model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: false); - // Caught on save, where the person configuring it is looking — the handler refuses this at + // Caught on save, where the person configuring it is looking - the handler refuses this at // send time too, but by then the only trace is a missing alert. await Assert.ThrowsAsync(() => new Create(db, ctx).Handle(model)); @@ -1365,7 +1365,7 @@ async Task Fail() => // on its own before any handler runs. scope.ServiceProvider.GetRequiredService().Revoke(); - // The subscription has no handler, so this exchange simply succeeds — an ordinary message + // The subscription has no handler, so this exchange simply succeeds - an ordinary message // getting through after the outage, which is the only evidence of recovery there is. var recovered = await xs.CreateXchange(sub, new XchangeFile("{}")); await db.SaveChangesAsync(); @@ -1452,7 +1452,7 @@ await runScope.ServiceProvider.GetRequiredService() /// Bitween runs several instances, so a failure can claim a slot while a success is still being /// processed. Releasing that row would give back a slot already spent and let the group retry past /// its total. The row's last attempt is compared against the successful exchange's start, which is - /// what this drives directly — the timing is otherwise a race no test could pin down. + /// what this drives directly - the timing is otherwise a race no test could pin down. /// [Fact] public async Task A_slot_charged_after_the_success_began_is_not_handed_back() diff --git a/SW.Bitween.IntegrationTests/Tests/RunFlagUpdaterTests.cs b/SW.Bitween.IntegrationTests/Tests/RunFlagUpdaterTests.cs index 17040fee..c0b65b30 100644 --- a/SW.Bitween.IntegrationTests/Tests/RunFlagUpdaterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RunFlagUpdaterTests.cs @@ -1,3 +1,6 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using SW.Bitween.Domain; @@ -7,9 +10,8 @@ namespace SW.Bitween.IntegrationTests.Tests; -// The run flag is written with raw (parameterized) SQL that differs per database -// provider, so it needs a real round trip to prove the statement and its parameter -// binding are correct. There was no coverage here before. +// The run flag is claimed with one conditional UPDATE; these prove it against a real database, +// including what happens when runners race and when a run dies holding it. [Collection("Bitween")] public class RunFlagUpdaterTests(BitweenFixture fixture) { @@ -62,4 +64,60 @@ public async Task Run_flag_only_affects_the_requested_subscription() await runFlag.MarkAsIdle(b.Id); } + + /// + /// A run killed with its process never clears the flag. It used to stay set for good, and the + /// subscription never ran again until someone edited the database. + /// + [Fact] + public async Task A_flag_left_by_a_run_that_died_is_taken_over() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var runFlag = scope.ServiceProvider.GetRequiredService(); + + var document = new Document(6103, "Run Flag Stale Doc"); + db.Set().Add(document); + var subscription = new Subscription("Run Flag Stale", document.Id) { Inactive = false }; + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + Assert.True(await runFlag.MarkAsRunning(subscription.Id)); + // The run dies: nobody calls MarkAsIdle, and the clock moves past the limit. + var longAgo = DateTime.UtcNow.AddHours(-5); + await db.Set().Where(s => s.Id == subscription.Id) + .ExecuteUpdateAsync(u => u.SetProperty(s => s.RunningSince, longAgo)); + + Assert.True(await runFlag.MarkAsRunning(subscription.Id)); + Assert.False(await runFlag.MarkAsRunning(subscription.Id)); // the new run now holds it + await runFlag.MarkAsIdle(subscription.Id); + } + + /// One conditional UPDATE, so however many runners race, exactly one claims it. + [Fact] + public async Task Of_many_concurrent_claims_exactly_one_wins() + { + int id; + await using (var setup = fixture.CreateScope()) + { + var db = setup.ServiceProvider.GetRequiredService(); + var document = new Document(6104, "Run Flag Race Doc"); + db.Set().Add(document); + var subscription = new Subscription("Run Flag Race", document.Id) { Inactive = false }; + db.Set().Add(subscription); + await db.SaveChangesAsync(); + id = subscription.Id; + } + + var claims = await Task.WhenAll(Enumerable.Range(0, 10).Select(async _ => + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService().MarkAsRunning(id); + })); + + Assert.Equal(1, claims.Count(c => c)); + + await using var cleanup = fixture.CreateScope(); + await cleanup.ServiceProvider.GetRequiredService().MarkAsIdle(id); + } } diff --git a/SW.Bitween.IntegrationTests/Tests/ScriptAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/ScriptAdapterTests.cs new file mode 100644 index 00000000..7f664eac --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ScriptAdapterTests.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SW.Bitween.Controllers; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.Bitween.Services.Adapters; +using SW.PrimitiveTypes; +using SW.Serverless.Tooling; +using SW.Serverless.Tooling.Building; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Adapters written in Python and JavaScript, built and published as serverless build and publish +/// do it - so only to their versions and the catalog, never to adapters/{id} - then listed, +/// described from their manifests, and run through subscriptions with their settings, exactly as +/// .NET ones are. +/// +[Collection("Bitween")] +public class ScriptAdapterTests(BitweenFixture fixture) +{ + static int _seq; + static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; + + static readonly SemaphoreSlim Published = new(1, 1); + static bool _published; + + /// Builds and publishes both Python adapters once for the class. + async Task PublishAsync() + { + await Published.WaitAsync(); + try + { + if (_published) return; + await using var scope = fixture.CreateScope(); + var files = scope.ServiceProvider.GetRequiredService(); + foreach (var (folder, project) in new[] { ("PythonAdapters", "orders"), ("PythonAdapters", "checks"), ("NodeAdapters", "orders") }) + { + var source = Path.Combine(AppContext.BaseDirectory, folder, project); + var work = Path.Combine(Path.GetTempPath(), "bitween-python", Guid.NewGuid().ToString("N")); + CopyFolder(source, Path.Combine(work, folder, project)); + + var built = await PackageBuilder.BuildAsync(new BuildRequest + { + Packages = SW.Bitween.Adapters.Tooling.BitweenAdapters.Packages(), + ProjectDirectory = Path.Combine(work, folder, project), + OutputDirectory = Path.Combine(work, "out"), + }); + Assert.True(built.Succeeded, string.Join("; ", built.Problems)); + + var published = await PackagePublisher.PublishPackageAsync(files, new PublishPackageRequest + { + PackagePath = built.ZipPath, + PublishedBy = "tests", + }, _ => { }); + scope.ServiceProvider.GetRequiredService().Forget(published.Manifest.Id); + } + _published = true; + } + finally + { + Published.Release(); + } + } + + static void CopyFolder(string from, string to) + { + foreach (var file in Directory.EnumerateFiles(from, "*", SearchOption.AllDirectories)) + { + var target = Path.Combine(to, Path.GetRelativePath(from, file)); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file, target); + } + } + + async Task CreateSubscription(SubscriptionType type, Action configure) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var document = new Document(null, Unique("Python doc"), DocumentFormat.Json); + var partner = new Partner(Unique("Python partner")); + db.AddRange(document, partner); + await db.SaveChangesAsync(); + + scope.Superuser(); + var request = new SubscriptionCreate + { + Name = Unique("Python subscription"), + DocumentId = document.Id, + PartnerId = partner.Id, + Type = type, + }; + configure(request); + var id = (int)await ActivatorUtilities.CreateInstance(scope.ServiceProvider).Handle(request); + await db.Set().Where(s => s.Id == id).ExecuteUpdateAsync(u => u.SetProperty(s => s.Inactive, false)); + return id; + } + + async Task<(XchangeResult Result, string Response)> Run(int subscriptionId, string input) + { + string xchangeId; + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + xchangeId = await scope.ServiceProvider.GetRequiredService() + .SubmitSubscriptionXchange(subscriptionId, new XchangeFile(input)); + } + + await using (var scope = fixture.CreateScope()) + await scope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + + await using (var scope = fixture.CreateScope()) + { + var result = await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(r => r.Id == xchangeId); + var service = scope.ServiceProvider.GetRequiredService(); + string response = null; + try { response = await service.GetFile(xchangeId, XchangeFileType.Response); } catch { } + return (result, response); + } + } + + [Fact] + public async Task A_python_handler_is_listed_with_the_settings_its_code_declares() + { + await PublishAsync(); + await using var scope = fixture.CreateScope(); + scope.Superuser(); + var rows = JArray.FromObject(await ActivatorUtilities.CreateInstance(scope.ServiceProvider) + .Handle(new AdapterSearchRequest { Prefix = "handlers" })); + var row = rows.Single(r => (string)r["Key"] == "infolink6.handlers.pyorders"); + + Assert.Equal("python", (string)row["Runtime"]); + Assert.Equal("1.0.0", (string)row["CurrentVersion"]); + Assert.Equal("Python orders", (string)row["DisplayName"]); + Assert.False((bool)row["StartupValues"]!["Partner"]!["Optional"]); + Assert.True((bool)row["StartupValues"]!["Token"]!["Private"]); + // Its source came with it, so it can be read in Bitween. + Assert.True((bool)row["VersionHistory"]![0]!["HasSource"]); + } + + [Fact] + public async Task A_subscription_delivers_through_a_python_handler_with_its_settings() + { + await PublishAsync(); + var subscription = await CreateSubscription(SubscriptionType.ApiCall, s => + { + s.HandlerId = "infolink6.handlers.pyorders"; + s.HandlerProperties = + [ + new KeyAndValue { Key = "Partner", Value = "acme" }, + new KeyAndValue { Key = "Token", Value = "secret-123" }, + ]; + }); + + var (result, response) = await Run(subscription, "{\"orderId\":\"SO-7\"}"); + + Assert.True(result.Success, result.Exception); + Assert.False(result.ResponseBad); + var answer = JObject.Parse(response); + Assert.Equal("acme", (string)answer["to"]); + Assert.Equal(10, (int)answer["token"]); + Assert.Equal("SO-7", (string)answer["order"]!["orderId"]); + } + + [Fact] + public async Task A_subscription_delivers_through_a_node_handler_with_its_settings() + { + await PublishAsync(); + var subscription = await CreateSubscription(SubscriptionType.ApiCall, s => + { + s.HandlerId = "infolink6.handlers.nodeorders"; + s.HandlerProperties = + [ + new KeyAndValue { Key = "Partner", Value = "globex" }, + new KeyAndValue { Key = "Token", Value = "abc" }, + ]; + }); + + var (result, response) = await Run(subscription, "{\"orderId\":\"SO-9\"}"); + + Assert.True(result.Success, result.Exception); + var answer = JObject.Parse(response); + Assert.Equal("globex", (string)answer["to"]); + Assert.Equal(3, (int)answer["token"]); + Assert.Equal("SO-9", (string)answer["order"]!["orderId"]); + + var (rejected, body) = await Run(subscription, "{\"reject\":true}"); + Assert.True(rejected.ResponseBad); + Assert.Equal("{\"error\":\"rejected\"}", body); + } + + [Fact] + public async Task A_rejection_from_a_python_handler_is_a_bad_response_not_a_failure() + { + await PublishAsync(); + var subscription = await CreateSubscription(SubscriptionType.ApiCall, s => + { + s.HandlerId = "infolink6.handlers.pyorders"; + s.HandlerProperties = [new KeyAndValue { Key = "Partner", Value = "acme" }, new KeyAndValue { Key = "Token", Value = "t" }]; + }); + + var (result, response) = await Run(subscription, "{\"reject\":true}"); + + Assert.True(result.ResponseBad); + Assert.Equal("{\"error\":\"rejected\"}", response); + } + + /// A gateway whose subscription is validated by the Python validator, as ValidatorStepTests builds one. + async Task<(string UrlName, string Key, int SubscriptionId)> ValidatedGateway() + { + var urlName = Unique("pyvalidated").ToLowerInvariant(); + var key = Guid.NewGuid().ToString("N"); + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner(Unique("Python validated partner")); + partner.SetApiCredentials([new ApiCredential("main", key)]); + var doc = new Document(null, Unique("Python validated doc"), DocumentFormat.Json); + db.AddRange(partner, doc); + await db.SaveChangesAsync(); + var subscription = new Subscription(Unique("Python validated sub"), doc.Id, SubscriptionType.GatewayApiCall) + { + Inactive = false, + ValidatorId = "infolink6.validators.pychecks", + }; + db.Add(subscription); + await db.SaveChangesAsync(); + db.Add(new ApiGateway + { + Name = Unique("Python validated gateway"), + UrlName = urlName, + Partners = [new ApiGatewayPartner { PartnerId = partner.Id, SubscriptionId = subscription.Id }], + }); + await db.SaveChangesAsync(); + return (urlName, key, subscription.Id); + } + + async Task Call(string urlName, string key, string body) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Revoke(); + scope.ServiceProvider.GetRequiredService().Set( + new ClaimsPrincipal(new ClaimsIdentity()), + [new RequestValue("partnerkey", key, RequestValueType.HttpHeader)]); + var controller = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + controller.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + return await controller.Post($"{urlName}/async"); + } + + [Fact] + public async Task A_python_validator_refuses_a_gateway_call_with_its_reasons_and_lets_a_good_one_through() + { + await PublishAsync(); + var (urlName, key, subscriptionId) = await ValidatedGateway(); + + var refused = await Assert.ThrowsAsync(() => Call(urlName, key, "{\"lines\":1}")); + Assert.Equal("An order needs an id.", refused.Validations.Single(v => v.Key == "orderId").Value); + + Assert.IsType(await Call(urlName, key, "{\"orderId\":\"SO-8\"}")); + await using var scope = fixture.CreateScope(); + Assert.Equal(1, await scope.ServiceProvider.GetRequiredService() + .Set().CountAsync(x => x.SubscriptionId == subscriptionId)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/SecretColumnEncryptionTests.cs b/SW.Bitween.IntegrationTests/Tests/SecretColumnEncryptionTests.cs new file mode 100644 index 00000000..4d3b5877 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/SecretColumnEncryptionTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// With an encryption key configured, credential columns hold ciphertext in the database and +/// plain values in the application; without one, nothing changes. +/// +[Collection("Bitween")] +public class SecretColumnEncryptionTests(BitweenFixture fixture) +{ + private const string Secret = "partner-password-in-clear"; + + [Fact] + public async Task A_partner_credential_is_stored_encrypted_and_read_back_plain() + { + SecretColumnCipher.Configure("integration test passphrase"); + try + { + var partnerId = await CreatePartnerAsync(); + + Assert.DoesNotContain(Secret, await RawAdapterPropertiesAsync(partnerId)); + Assert.Equal(Secret, (await ReadPartnerAsync(partnerId)).AdapterProperties["password"]); + } + finally + { + SecretColumnCipher.Configure(null); + } + } + + [Fact] + public async Task A_credential_saved_before_encryption_is_encrypted_by_the_pass() + { + var partnerId = await CreatePartnerAsync(); // no key yet: stored in clear + Assert.Contains(Secret, await RawAdapterPropertiesAsync(partnerId)); + + SecretColumnCipher.Configure("integration test passphrase"); + try + { + var pass = ActivatorUtilities.CreateInstance(fixture.App.Services); + await pass.RunAsync(CancellationToken.None); + + Assert.DoesNotContain(Secret, await RawAdapterPropertiesAsync(partnerId)); + Assert.Equal(Secret, (await ReadPartnerAsync(partnerId)).AdapterProperties["password"]); + } + finally + { + // Everything the pass encrypted would be unreadable to the rest of the collection, so it + // is written back in clear: with no key and the old one as previous, values are read + // with the old key and written unencrypted. + var decrypt = ActivatorUtilities.CreateInstance(fixture.App.Services); + SecretColumnCipher.Configure(null, previousPassphrase: "integration test passphrase"); + await decrypt.RunAsync(CancellationToken.None); + SecretColumnCipher.Configure(null); + } + } + + private async Task CreatePartnerAsync() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner($"Encrypted {Guid.NewGuid():N}") + { + AdapterProperties = new Dictionary { ["password"] = Secret }, + }; + db.Add(partner); + await db.SaveChangesAsync(); + return partner.Id; + } + + private async Task ReadPartnerAsync(int id) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService() + .Set().AsNoTracking().SingleAsync(p => p.Id == id); + } + + private async Task RawAdapterPropertiesAsync(int id) + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await db.Database + .SqlQueryRaw("SELECT adapter_properties::text AS \"Value\" FROM infolink.partner WHERE id = {0}", id) + .SingleAsync(); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/SeededAdministratorTests.cs b/SW.Bitween.IntegrationTests/Tests/SeededAdministratorTests.cs new file mode 100644 index 00000000..8e0fcb69 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/SeededAdministratorTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Services; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// The administrator the migrations seed into every installation, with a password published in +/// this repository. Each test runs inside a transaction that is rolled back, because the seeded +/// account is shared by the whole collection. +/// +[Collection("Bitween")] +public class SeededAdministratorTests(BitweenFixture fixture) +{ + private const string Chosen = "Chosen-At-Install-9!"; + + private static Task Admin(BitweenDbContext db) + { + db.ChangeTracker.Clear(); + return db.Set().AsNoTracking().SingleAsync(a => a.Id == SeededAdministrator.Id); + } + + [Fact] + public async Task A_configured_password_replaces_the_published_one_and_ends_its_sessions() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + db.Set().Add(new RefreshToken(SeededAdministrator.Id, LoginMethod.EmailAndPassword)); + await db.SaveChangesAsync(); + + await SeededAdministrator.Secure(db, Chosen, NullLogger.Instance); + + var admin = await Admin(db); + Assert.True(SecurePasswordHasher.Verify(Chosen, admin.Password)); + Assert.False(admin.MustChangePassword); + Assert.Empty(db.Set().Where(t => t.AccountId == SeededAdministrator.Id)); + } + + [Fact] + public async Task Without_one_a_new_installation_refuses_to_start() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + // Other tests leave accounts behind; a new installation has only the seeded one. + await db.Set().Where(a => a.Id != SeededAdministrator.Id) + .ExecuteUpdateAsync(s => s.SetProperty(a => a.Deleted, true)); + + var refused = await Assert.ThrowsAsync(() => + SeededAdministrator.Secure(db, null, NullLogger.Instance)); + Assert.Contains("Bitween__InitialAdminPassword", refused.Message); + } + + [Fact] + public async Task Without_one_an_installation_with_other_accounts_starts_unchanged() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + db.Set().Add(new Account("Other", $"other-{Guid.NewGuid():N}@test.local", "hash", AccountRole.Member)); + await db.SaveChangesAsync(); + + await SeededAdministrator.Secure(db, null, NullLogger.Instance); + + Assert.True(SecurePasswordHasher.Verify(SeededAdministrator.PublishedPassword, (await Admin(db)).Password)); + } + + [Fact] + public async Task The_published_password_is_refused_as_the_configured_one() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + await Assert.ThrowsAsync(() => + SeededAdministrator.Secure(db, SeededAdministrator.PublishedPassword, NullLogger.Instance)); + } + + [Fact] + public async Task Once_the_password_has_changed_the_configured_one_is_ignored() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + var admin = await db.Set().SingleAsync(a => a.Id == SeededAdministrator.Id); + admin.SetPassword("Chosen-Long-Ago-9!"); + await db.SaveChangesAsync(); + + await SeededAdministrator.Secure(db, Chosen, NullLogger.Instance); + + Assert.True(SecurePasswordHasher.Verify("Chosen-Long-Ago-9!", (await Admin(db)).Password)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/SettingsTests.cs b/SW.Bitween.IntegrationTests/Tests/SettingsTests.cs index b301e2f2..84af6ce3 100644 --- a/SW.Bitween.IntegrationTests/Tests/SettingsTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SettingsTests.cs @@ -13,11 +13,11 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// Instance settings — the values an administrator can change without a redeploy. +/// Instance settings - the values an administrator can change without a redeploy. /// /// /// Two things make this worth covering. Secrets are encrypted before they are written, so a -/// database dump never carries a license key in the clear — and an encryption bug is invisible +/// database dump never carries a license key in the clear - and an encryption bug is invisible /// from the UI, which shows the value correctly either way. And only some settings are editable at /// all: the page also lists environment-owned ones, and accepting a write to those would report /// success for a change that can never take effect. @@ -35,7 +35,7 @@ public class SettingsTests(BitweenFixture fixture) : IAsyncLifetime /// /// Applying a setting mutates a process-wide options singleton that every test in this /// collection shares, so anything written here has to be put back. Nothing depends on these - /// two today — but the Rebex key decides which adapters are offered at all, and a future test + /// two today - but the Rebex key decides which adapters are offered at all, and a future test /// of that would pass or fail purely on whether this class happened to run first. /// public async Task InitializeAsync() @@ -117,7 +117,7 @@ public async Task An_ordinary_setting_takes_effect_immediately_and_is_stored_as_ await Store(EditableKey, "45"); // Applied to the live options singleton as well as stored, so the very next request - // already sees it — there is no restart-required concept in this product. + // already sees it - there is no restart-required concept in this product. Assert.Equal("45", await LiveValue(EditableKey)); Assert.Equal("45", await RawStored(EditableKey)); } @@ -151,7 +151,7 @@ public async Task A_value_the_setting_cannot_hold_is_refused() { var ex = await Assert.ThrowsAsync(() => Store(EditableKey, "not-a-number")); - // Stored unchecked, this fails much later — when something reads the session length and + // Stored unchecked, this fails much later - when something reads the session length and // cannot parse it, far from the screen where it was typed. Assert.StartsWith("SETTING_INVALID_VALUE", ex.Message); } diff --git a/SW.Bitween.IntegrationTests/Tests/SharedBrokerTests.cs b/SW.Bitween.IntegrationTests/Tests/SharedBrokerTests.cs index 8c03ae16..a228c604 100644 --- a/SW.Bitween.IntegrationTests/Tests/SharedBrokerTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SharedBrokerTests.cs @@ -23,7 +23,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// One customer broker, several integrations — which is what a real one looks like. +/// One customer broker, several integrations - which is what a real one looks like. /// /// Everything else in this suite exercises a data source with a single queue behind it. That is /// the easy case and it hides the questions that actually matter once a second integration lands @@ -44,7 +44,7 @@ public class SharedBrokerTests(BitweenFixture fixture) /// The multiplexing claim, stated as a number: three gateways, one process. /// /// If each gateway opened its own connection, a customer with twenty integrations on one broker - /// would have Bitween holding twenty connections and twenty adapter processes — and every one + /// would have Bitween holding twenty connections and twenty adapter processes - and every one /// of them would need its own lease, its own restart budget and its own credentials in memory. /// The whole point of separating the connection from what is done with it is that this stays /// one of each. @@ -119,7 +119,7 @@ await CountAsync(invoices.DocumentId) >= each && await CountAsync(shipments.DocumentId) >= each, TimeSpan.FromSeconds(90), "the queues did not all drain"); - // Settle, then assert nothing extra landed anywhere — a crossed-over message shows up as a + // Settle, then assert nothing extra landed anywhere - a crossed-over message shows up as a // surplus on one document and a shortfall on another. await Task.Delay(TimeSpan.FromSeconds(3)); @@ -139,7 +139,7 @@ await CountAsync(shipments.DocumentId) >= each, /// /// The customer's broker is not Bitween's broker. /// - /// Acme runs other things on it — a queue their warehouse app drains, another their own + /// Acme runs other things on it - a queue their warehouse app drains, another their own /// services talk over. Bitween is pointed at three queues and must touch nothing else: an /// adapter that consumed everything it could see would silently eat another system's traffic, /// and the first anyone would know is when the warehouse stopped receiving orders. @@ -171,7 +171,7 @@ await WaitAsync(async () => await CountAsync(orders.DocumentId) == 1, /// /// A fourth integration goes live on Monday. The three already running must not notice. /// - /// The adapter restarts to pick up the new consume list — that is expected, and safe, because + /// The adapter restarts to pick up the new consume list - that is expected, and safe, because /// nothing is acknowledged until Bitween has persisted it. What must NOT happen is the other /// three losing messages or needing to be reconfigured. /// @@ -210,9 +210,9 @@ await CountAsync(invoices.DocumentId) == 1, /// /// Turning one integration off leaves the rest running. /// - /// Deactivating a gateway has to stop its queue being consumed — otherwise Bitween keeps + /// Deactivating a gateway has to stop its queue being consumed - otherwise Bitween keeps /// acknowledging messages that no longer route anywhere, which is a silent drop rather than a - /// pause — while every other queue on the same connection carries on. + /// pause - while every other queue on the same connection carries on. /// [Fact] public async Task Deactivating_one_gateway_stops_only_its_queue() @@ -239,7 +239,7 @@ await CountAsync(orders.DocumentId) == 1 && await CountAsync(invoices.DocumentId await WaitAsync(async () => await CountAsync(orders.DocumentId) == 2, TimeSpan.FromSeconds(60), "the active integration stopped working too"); - // The deactivated one's message is still on the broker, waiting — not acknowledged and + // The deactivated one's message is still on the broker, waiting - not acknowledged and // thrown away, which is the outcome that would look identical from Bitween's side. await Task.Delay(TimeSpan.FromSeconds(3)); Assert.Equal(1, await CountAsync(invoices.DocumentId)); @@ -252,7 +252,7 @@ await WaitAsync(async () => await CountAsync(orders.DocumentId) == 2, /// /// A broker's message id is only unique within whatever produced it, so two systems publishing /// to two queues on one broker can easily pick the same one. Deduplicating across the whole - /// data source would silently drop the second — so the key carries the endpoint. + /// data source would silently drop the second - so the key carries the endpoint. /// [Fact] public async Task The_same_message_id_on_two_queues_is_two_messages() @@ -271,7 +271,7 @@ public async Task The_same_message_id_on_two_queues_is_two_messages() await WaitAsync(async () => await CountAsync(orders.DocumentId) == 1 && await CountAsync(invoices.DocumentId) == 1, TimeSpan.FromSeconds(60), - "one of the two was deduplicated away — the key is not scoped to the endpoint"); + "one of the two was deduplicated away - the key is not scoped to the endpoint"); // And a genuine redelivery on ONE of them still is a duplicate. Publish(orders.Queue, """{"from":"orders"}""", shared); @@ -323,7 +323,7 @@ await CountAsync(contosoOrders.DocumentId) == 1, /// The point of all of it: three queues on one broker running three different integrations, /// each producing its own result. /// - /// Everything above is about plumbing. This is the outcome the plumbing exists for — a message + /// Everything above is about plumbing. This is the outcome the plumbing exists for - a message /// arriving on Acme's broker runs the subscription configured for that queue, and the result is /// recorded against the right one. /// @@ -361,7 +361,7 @@ public async Task Each_queue_runs_its_own_integration_and_records_its_own_result private sealed record Integration(string Queue, int DocumentId, int GatewayId, int SubscriptionId); - /// A data source with connection settings only — the endpoints come from its gateways. + /// A data source with connection settings only - the endpoints come from its gateways. private async Task BrokerAsync(string label) { await using var scope = fixture.CreateScope(); @@ -380,7 +380,7 @@ private async Task BrokerAsync(string label) return dataSource.Id; } - /// One queue, one information type, one gateway — and optionally a subscription behind it. + /// One queue, one information type, one gateway - and optionally a subscription behind it. private async Task IntegrationAsync(int dataSourceId, string label, bool withSubscription = false) { @@ -397,7 +397,7 @@ private async Task IntegrationAsync(int dataSourceId, string label, if (withSubscription) { // Every Subscription constructor sets Inactive = true, so it has to be turned on or - // the filter never matches it — see PipelineEndToEndTests. + // the filter never matches it - see PipelineEndToEndTests. var subscription = new Subscription(Unique($"sub-{label}"), document.Id, SubscriptionType.BusGateway) { @@ -531,7 +531,7 @@ await WaitAsync(async () => /// /// A supervisor that cleans up after itself. Only instances that appeared after it was built - /// are stopped — the collection shares one resident host, so stopping "whatever is running" + /// are stopped - the collection shares one resident host, so stopping "whatever is running" /// would reach into another test. /// private sealed class TestSupervisor : IAsyncDisposable diff --git a/SW.Bitween.IntegrationTests/Tests/SqlServerAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/SqlServerAdapterTests.cs index 62c68fe4..79718e4f 100644 --- a/SW.Bitween.IntegrationTests/Tests/SqlServerAdapterTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SqlServerAdapterTests.cs @@ -17,8 +17,8 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// The same suite as the others, which is the point: four engines run on one core, so the parts /// that are shared should behave identically and the parts that differ should differ VISIBLY. -/// SQL Server is the one where the shared statement CHECK could not be shared — its driver refuses -/// to prepare a command whose parameters have no explicit type — so the check has its own tests +/// SQL Server is the one where the shared statement CHECK could not be shared - its driver refuses +/// to prepare a command whose parameters have no explicit type - so the check has its own tests /// here rather than relying on the core's. /// [Collection("Bitween")] @@ -79,8 +79,8 @@ public async Task Connection_test_reports_each_stage_and_checks_every_statement( } /// - /// SQL Server is the most capable of the four on paper, and the list says so where it is true — - /// MERGE, OUTPUT and snapshot isolation are all real here — and stays false where the thing + /// SQL Server is the most capable of the four on paper, and the list says so where it is true - + /// MERGE, OUTPUT and snapshot isolation are all real here - and stays false where the thing /// exists but is not wired, which is Service Broker's query notifications. /// [SkippableFact] @@ -136,7 +136,7 @@ public async Task Discover_finds_the_table_with_its_columns_and_key() // An extended property, which is where SQL Server keeps what other engines call a comment. Assert.Equal("Customer orders", table.Value("comment")); - // The partition stats, not COUNT(*) — so this is an estimate, and asserting an exact + // The partition stats, not COUNT(*) - so this is an estimate, and asserting an exact // number would be asserting something the adapter deliberately does not promise. Other // tests in this class insert rows too, which is the second reason: 25 is the floor. Assert.True(table.Value("rowCount") >= 25, @@ -148,7 +148,7 @@ public async Task Discover_finds_the_table_with_its_columns_and_key() Assert.False(columns[0].Value("nullable")); // max_length is in BYTES and an nvarchar stores two per character, so the declared length - // is half of it — 50, not 100. + // is half of it - 50, not 100. var customer = columns.Single(c => c.Value("name") == "customer"); Assert.Equal("nvarchar(50)", customer.Value("dbType")); Assert.Equal(50, customer.Value("length")); @@ -200,7 +200,7 @@ public async Task Discover_finds_procedures_functions_and_sequences() Assert.Equal("p_total", parameters[1].Value("name")); Assert.Equal("Out", parameters[1].Value("direction")); - // An inline table-valued function is a function, not a procedure — the shape a receive + // An inline table-valued function is a function, not a procedure - the shape a receive // statement would select from. var functions = await adapter.InvokeAsync("Discover", new { objectType = "function", schema = "sales" }, timeoutSeconds: 60); @@ -246,7 +246,7 @@ public async Task Call_returns_rows_straight_from_a_procedure() } /// - /// OUTPUT is SQL Server's RETURNING, and the capability list claims it — so a statement using + /// OUTPUT is SQL Server's RETURNING, and the capability list claims it - so a statement using /// it has to actually work, not merely be declared possible. /// [SkippableFact] @@ -417,7 +417,7 @@ public async Task Mark_processed_runs_for_each_accepted_row() /// /// The check that could not be shared. SqlCommand.Prepare refuses unless every parameter has an - /// explicit type, which a caller checking someone else's SQL does not know — so this adapter + /// explicit type, which a caller checking someone else's SQL does not know - so this adapter /// asks sp_describe_undeclared_parameters instead, and these prove it answers the same /// questions the prepared form does everywhere else. /// @@ -516,7 +516,7 @@ async Task CreateDataSourceAsync() ["MaxPoolSize"] = "5", // The container presents a self-signed certificate, which is the case this setting exists - // for — and the reason the adapter offers it rather than pretending every server has a + // for - and the reason the adapter offers it rather than pretending every server has a // certificate chain that validates. ["Encrypt"] = "true", ["TrustServerCertificate"] = "true", @@ -539,7 +539,7 @@ async Task CreateDataSourceAsync() ["ReceiveMode"] = "incrementing", - // TOP rather than LIMIT, and it has to come before the column list — one of the small + // TOP rather than LIMIT, and it has to come before the column list - one of the small // dialect differences a statement writer meets immediately. ["ReceiveStatement"] = $"select top 5 * from sales.{SqlServerDbFixture.Table} where id > @cursor order by id", diff --git a/SW.Bitween.IntegrationTests/Tests/SqsBusGatewayTests.cs b/SW.Bitween.IntegrationTests/Tests/SqsBusGatewayTests.cs index feb0e425..6927b896 100644 --- a/SW.Bitween.IntegrationTests/Tests/SqsBusGatewayTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SqsBusGatewayTests.cs @@ -31,7 +31,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// receive -> persist the Xchange -> ONLY THEN DeleteMessage /// /// So the tests here are about deletion and visibility rather than ack and nack, and about the -/// Selling Partner API envelope — because SP-API delivers notifications by publishing to an SQS +/// Selling Partner API envelope - because SP-API delivers notifications by publishing to an SQS /// queue you own, which is the reason this provider exists at all. /// [Collection("Bitween")] @@ -101,7 +101,7 @@ await WaitAsync(async () => var stats = await adapter.Instance.InvokeAsync("GetStats"); - // Unclaimed is accepted-and-discarded on purpose — rejecting would loop for ever — so the + // Unclaimed is accepted-and-discarded on purpose - rejecting would loop for ever - so the // message is deleted. What must NOT happen is a silent failure that leaves it invisible. Assert.Equal(0, stats.Value("failed")); Assert.True(stats.Value("deleted") > 0 || stats.Value("returned") > 0, @@ -159,7 +159,7 @@ public async Task A_selling_partner_notification_is_unwrapped_to_its_payload() /// /// The dedupe key reaches the Xchange as a reference, and for an SP-API notification it is the - /// notification id — which is stable across a redelivery where the SQS MessageId is not. + /// notification id - which is stable across a redelivery where the SQS MessageId is not. /// /// NOTE THE GAP THIS DOES NOT COVER. Nothing in Bitween currently ENFORCES uniqueness on that /// reference, so a redelivered message produces a second Xchange. The key is carried, which is @@ -279,7 +279,7 @@ public async Task A_delivery_with_no_endpoint_says_so() /// /// The multi-node case. A broker data source is exclusive, but a delivery runs on whichever - /// node picked the message up — so when the owned instance is not here the runtime opens a + /// node picked the message up - so when the owned instance is not here the runtime opens a /// send-only connection of its own rather than failing. See the RabbitMQ twin of this test /// for the reasoning in full. /// @@ -359,7 +359,7 @@ private async Task ArrangeAsync(string queueUrl, bool unwrapSpApi = false }); await db.SaveChangesAsync(); - // Ten-minute singleton snapshot — without this the document is invisible to the pipeline. + // Ten-minute singleton snapshot - without this the document is invisible to the pipeline. scope.ServiceProvider.GetRequiredService().Revoke(); return new Setup(document.Id, dataSourceId); diff --git a/SW.Bitween.IntegrationTests/Tests/SubscriptionLifecycleTests.cs b/SW.Bitween.IntegrationTests/Tests/SubscriptionLifecycleTests.cs index 8e6139fa..da8cc4e7 100644 --- a/SW.Bitween.IntegrationTests/Tests/SubscriptionLifecycleTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SubscriptionLifecycleTests.cs @@ -14,14 +14,14 @@ namespace SW.Bitween.IntegrationTests.Tests; /// -/// Creating, changing and removing an integration — the entity the whole product is arranged +/// Creating, changing and removing an integration - the entity the whole product is arranged /// around, and the one the redesigned UI touches on nearly every screen. /// /// /// The delete path gets most of the attention here. All four references are RESTRICT in the /// database, so a delete was always refused; what the handler adds is saying what is holding /// it instead of letting a foreign key violation surface as a 500. That message is the whole -/// feature, so these tests assert on its content rather than just on the refusal — a check that +/// feature, so these tests assert on its content rather than just on the refusal - a check that /// only asserted "it threw" would still pass if the message went back to being useless. /// [Collection("Bitween")] @@ -101,7 +101,7 @@ public async Task An_integration_can_be_created_changed_and_removed() /// /// Sounds too small to test, and it is exactly the field a merge dropped from the projection /// once. Nothing failed: the API answered with dataSourceId null while the row held an id, so - /// the UI read every bound subscription as unbound — and saving one from that screen wrote the + /// the UI read every bound subscription as unbound - and saving one from that screen wrote the /// null back and quietly unbound it. A projection is only as good as the thing that notices a /// field missing from it. /// @@ -157,7 +157,7 @@ public async Task Deleting_names_the_bus_gateway_route_still_pointing_at_it() var ex = await Assert.ThrowsAsync(() => Delete(id)); - // The operator has to be able to go straight to the thing holding it — a bare refusal + // The operator has to be able to go straight to the thing holding it - a bare refusal // sends them hunting through every gateway by hand. Assert.Contains("Orders bus", ex.Message); Assert.Contains("route", ex.Message); diff --git a/SW.Bitween.IntegrationTests/Tests/SubscriptionSecretTests.cs b/SW.Bitween.IntegrationTests/Tests/SubscriptionSecretTests.cs index ae02b2c7..f5becd78 100644 --- a/SW.Bitween.IntegrationTests/Tests/SubscriptionSecretTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/SubscriptionSecretTests.cs @@ -19,7 +19,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// Secrets are masked on the way out as __private__ and restored on the way back in. That /// leaves an obvious way to destroy one by accident: the browser sends back the dots it was shown, /// and a handler that took them literally would overwrite a working password with the placeholder -/// — breaking the integration at the next run, with nothing in the audit trail that looks like a +/// - breaking the integration at the next run, with nothing in the audit trail that looks like a /// password change. These tests hold that behaviour in place from both directions: the sentinel /// never lands in storage, and a real new value still does. /// @@ -88,15 +88,35 @@ public async Task Saving_the_mask_back_keeps_the_stored_secret() { var (id, documentId, partnerId) = await AnIntegrationWithASecret(); - // Exactly what the browser posts when someone edits the host and leaves the password alone. + // Exactly what the browser posts when someone edits the subject and leaves the password alone. await Update(id, documentId, partnerId, - new KeyAndValue { Key = "Host", Value = "smtp.newhost.com" }, + new KeyAndValue { Key = "Host", Value = "smtp.example.com" }, + new KeyAndValue { Key = "Subject", Value = "Renamed subject" }, new KeyAndValue { Key = "Password", Value = Sentinel }); var properties = await StoredProperties(id); Assert.Equal(RealPassword, properties["Password"]); - Assert.Equal("smtp.newhost.com", properties["Host"]); + Assert.Equal("Renamed subject", properties["Subject"]); + } + + /// + /// Except when the edit moves the integration to another server: the kept password would go to + /// whoever runs it, sent there by someone who never saw it. It has to be entered again. + /// + [Fact] + public async Task Moving_to_another_server_drops_the_kept_secret() + { + var (id, documentId, partnerId) = await AnIntegrationWithASecret(); + + await Update(id, documentId, partnerId, + new KeyAndValue { Key = "Host", Value = "smtp.attacker.example" }, + new KeyAndValue { Key = "Password", Value = Sentinel }); + + var properties = await StoredProperties(id); + + Assert.False(properties.ContainsKey("Password")); + Assert.Equal("smtp.attacker.example", properties["Host"]); } [Fact] @@ -134,7 +154,7 @@ public async Task Dropping_a_property_removes_it_rather_than_restoring_it() await Update(id, documentId, partnerId, new KeyAndValue { Key = "Host", Value = "smtp.example.com" }); - // Only a mask restores the stored value. A property the caller omits entirely is gone — + // Only a mask restores the stored value. A property the caller omits entirely is gone - // otherwise a setting could never be cleared once set. Assert.False((await StoredProperties(id)).ContainsKey("Password")); } @@ -150,7 +170,7 @@ await Update(id, documentId, partnerId, new KeyAndValue { Key = "ApiKey", Value = Sentinel }); // There is nothing to restore, so the sentinel must not be written through as if it were - // the value — that is how the literal string "__private__" ends up being sent as an API key. + // the value - that is how the literal string "__private__" ends up being sent as an API key. var properties = await StoredProperties(id); Assert.False(properties.ContainsKey("ApiKey")); Assert.Equal(RealPassword, properties["Password"]); diff --git a/SW.Bitween.IntegrationTests/Tests/SystemPartnerKeyTests.cs b/SW.Bitween.IntegrationTests/Tests/SystemPartnerKeyTests.cs new file mode 100644 index 00000000..b1de6cfc --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/SystemPartnerKeyTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Services; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// The SYSTEM partner's API key the migrations seed, published in this repository. Each test runs +/// inside a transaction that is rolled back, because the SYSTEM partner is shared by the whole +/// collection. +/// +[Collection("Bitween")] +public class SystemPartnerKeyTests(BitweenFixture fixture) +{ + private const string Configured = "configured-system-key-0123456789abcdef"; + + // Stored either way: hashed by the startup pass, or as seeded on a database it has not run on. + private static bool IsPublished(string stored) => + stored == SystemPartnerKey.PublishedKey || stored == PartnerKeyHash.Of(SystemPartnerKey.PublishedKey); + + private static async Task Keys(BitweenDbContext db) + { + db.ChangeTracker.Clear(); + var partner = await db.Set().AsNoTracking().SingleAsync(p => p.Id == Partner.SystemId); + return partner.ApiCredentials.Select(c => c.Key).ToArray(); + } + + [Fact] + public async Task A_configured_key_replaces_the_published_one() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + await SystemPartnerKey.Secure(db, Configured, NullLogger.Instance); + + var keys = await Keys(db); + Assert.Contains(PartnerKeyHash.Of(Configured), keys); + Assert.DoesNotContain(keys, IsPublished); + } + + [Fact] + public async Task Without_one_a_new_installation_gets_a_random_key() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + // Other tests leave accounts behind; a new installation has only the seeded one. + await db.Set().Where(a => a.Id != SeededAdministrator.Id) + .ExecuteUpdateAsync(s => s.SetProperty(a => a.Deleted, true)); + + await SystemPartnerKey.Secure(db, null, NullLogger.Instance); + + var keys = await Keys(db); + Assert.Single(keys); + Assert.False(IsPublished(keys[0])); + Assert.True(PartnerKeyHash.IsHashed(keys[0])); + } + + [Fact] + public async Task Without_one_an_existing_installation_keeps_the_published_key() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + db.Set().Add(new Account("Other", $"other-{Guid.NewGuid():N}@test.local", "hash", AccountRole.Member)); + await db.SaveChangesAsync(); + + await SystemPartnerKey.Secure(db, null, NullLogger.Instance); + + Assert.Contains(await Keys(db), IsPublished); + } + + [Fact] + public async Task The_published_key_is_refused_as_the_configured_one() + { + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); + + await Assert.ThrowsAsync(() => + SystemPartnerKey.Secure(db, SystemPartnerKey.PublishedKey, NullLogger.Instance)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/TeamGuardTests.cs b/SW.Bitween.IntegrationTests/Tests/TeamGuardTests.cs index 3b2688cc..9d195d66 100644 --- a/SW.Bitween.IntegrationTests/Tests/TeamGuardTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/TeamGuardTests.cs @@ -36,8 +36,8 @@ private static async Task CreateAccount(BitweenDbContext db, string lab /// /// The collection shares one database, and other tests leave administrators in it. So the - /// "only administrator" state is made inside a transaction — everyone else disabled, which the - /// guard doesn't count — and rolled back afterwards, so no other test ever sees it. + /// "only administrator" state is made inside a transaction - everyone else disabled, which the + /// guard doesn't count - and rolled back afterwards, so no other test ever sees it. /// [Fact] public async Task The_last_administrator_cannot_lose_the_role_be_disabled_or_be_removed() diff --git a/SW.Bitween.IntegrationTests/Tests/UnguardedEndpointTests.cs b/SW.Bitween.IntegrationTests/Tests/UnguardedEndpointTests.cs index 6070e1c3..1fd0ab9f 100644 --- a/SW.Bitween.IntegrationTests/Tests/UnguardedEndpointTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/UnguardedEndpointTests.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.IntegrationTests.Tests; /// /// Injecting the thing that checks permissions and then not calling it looks exactly like a /// handler that does check: the dependency is declared, the reader's eye stops there. The -/// compiler could not see it either, because an assigned-but-unread field is not a warning — it +/// compiler could not see it either, because an assigned-but-unread field is not a warning - it /// only surfaced once these became primary constructor parameters and the unused ones got named. /// /// So a viewer could rename the subscription categories everyone else files by, delete them, and @@ -97,6 +97,27 @@ await Assert.ThrowsAsync(() => })); } + /// + /// The rules-based preview maps against any partner the caller names, with every global value + /// set besides, and took no RequestContext at all. + /// + [Fact] + public async Task A_viewer_cannot_run_the_mapping_rules_preview() + { + await using var scope = fixture.CreateScope(); + await scope.AsNewViewer(Unique("rules-preview")); + + var preview = ActivatorUtilities.CreateInstance( + scope.ServiceProvider); + + await Assert.ThrowsAsync(() => + preview.Handle(new Resources.MappingPreviews.MappingPreviewRequest + { + MappingRules = "{}", + SourceDocument = "{}" + })); + } + private async Task CategoryAsAdmin() { await using var scope = fixture.CreateScope(); diff --git a/SW.Bitween.IntegrationTests/Tests/UnpauseTests.cs b/SW.Bitween.IntegrationTests/Tests/UnpauseTests.cs new file mode 100644 index 00000000..19c2065c --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/UnpauseTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Unpausing releases what was held while the subscription was paused. It is released in batches, +/// so a long pause can't load the whole backlog - payloads and all - into memory at once. +/// +[Collection("Bitween")] +public class UnpauseTests(BitweenFixture fixture) +{ + [Fact] + public async Task A_backlog_bigger_than_a_batch_is_released_in_full() + { + const int held = 120; + int subscriptionId; + await using (var scope = fixture.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var document = new Document(null, $"Unpause {Guid.NewGuid():N}", DocumentFormat.Json); + db.Add(document); + await db.SaveChangesAsync(); + + var subscription = new Subscription($"Unpause {Guid.NewGuid():N}", document.Id) { Inactive = false }; + db.Add(subscription); + await db.SaveChangesAsync(); + subscriptionId = subscription.Id; + + for (var i = 0; i < held; i++) + db.Add(new OnHoldXchange(subscription, $"{{\"n\":{i}}}")); + await db.SaveChangesAsync(); + } + + await using (var scope = fixture.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Revoke(); + await scope.ServiceProvider.GetRequiredService() + .Process(new SubscriptionUnpausedEvent { Id = subscriptionId }); + } + + await using var check = fixture.CreateScope(); + var checkDb = check.ServiceProvider.GetRequiredService(); + Assert.False(await checkDb.Set().AnyAsync(x => x.SubscriptionId == subscriptionId)); + Assert.Equal(held, await checkDb.Set().CountAsync(x => x.SubscriptionId == subscriptionId)); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ValidatorStepTests.cs b/SW.Bitween.IntegrationTests/Tests/ValidatorStepTests.cs new file mode 100644 index 00000000..08aba262 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ValidatorStepTests.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Controllers; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// A subscription's validator, run by the real serverless runtime on a gateway call: a payload it +/// rejects is refused while the partner is still on the line, with the validator's reasons, and +/// nothing is created; one it accepts goes through. +/// +[Collection("Bitween")] +public class ValidatorStepTests(BitweenFixture fixture) +{ + private static int _seq; + private static string Unique(string prefix) => $"{prefix}-{Interlocked.Increment(ref _seq)}"; + + private async Task<(string urlName, string key, int subscriptionId)> ValidatedGateway() + { + var urlName = Unique("validated").ToLowerInvariant(); + var key = Guid.NewGuid().ToString("N"); + + await using var scope = fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var partner = new Partner(Unique("Validated partner")); + partner.SetApiCredentials([new ApiCredential("main", key)]); + var doc = new Document(null, Unique("Validated doc"), DocumentFormat.Json); + db.AddRange(partner, doc); + await db.SaveChangesAsync(); + + var subscription = new Subscription(Unique("Validated sub"), doc.Id, SubscriptionType.GatewayApiCall) + { + Inactive = false, + ValidatorId = BitweenFixture.SampleValidatorId, + }; + db.Add(subscription); + await db.SaveChangesAsync(); + + db.Add(new ApiGateway + { + Name = Unique("Validated gateway"), + UrlName = urlName, + Partners = [new ApiGatewayPartner { PartnerId = partner.Id, SubscriptionId = subscription.Id }], + }); + await db.SaveChangesAsync(); + return (urlName, key, subscription.Id); + } + + private async Task Call(string urlName, string key, string body) + { + await using var scope = fixture.CreateScope(); + scope.ServiceProvider.GetRequiredService().Revoke(); + scope.ServiceProvider.GetRequiredService().Set( + new ClaimsPrincipal(new ClaimsIdentity()), + [new RequestValue("partnerkey", key, RequestValueType.HttpHeader)]); + + var controller = ActivatorUtilities.CreateInstance(scope.ServiceProvider); + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + controller.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + return await controller.Post($"{urlName}/async"); + } + + private async Task Exchanges(int subscriptionId) + { + await using var scope = fixture.CreateScope(); + return await scope.ServiceProvider.GetRequiredService() + .Set().CountAsync(x => x.SubscriptionId == subscriptionId); + } + + [Fact] + public async Task A_payload_the_validator_rejects_is_refused_with_its_reasons_and_creates_nothing() + { + var (urlName, key, subscriptionId) = await ValidatedGateway(); + + var refused = await Assert.ThrowsAsync(() => + Call(urlName, key, "{\"Id\":50}")); + + var reasons = refused.Validations.ToDictionary(v => v.Key, v => v.Value); + Assert.Contains("Id", reasons.Keys); + Assert.Contains("Name", reasons.Keys); + Assert.Equal(0, await Exchanges(subscriptionId)); + } + + [Fact] + public async Task A_payload_the_validator_accepts_goes_through() + { + var (urlName, key, subscriptionId) = await ValidatedGateway(); + + Assert.IsType(await Call(urlName, key, "{\"Id\":3,\"Name\":\"Widget\"}")); + Assert.Equal(1, await Exchanges(subscriptionId)); + } +} diff --git a/SW.Bitween.MsSql/BitweenDbContext.cs b/SW.Bitween.MsSql/BitweenDbContext.cs index 931ab86e..c952d14a 100644 --- a/SW.Bitween.MsSql/BitweenDbContext.cs +++ b/SW.Bitween.MsSql/BitweenDbContext.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.MsSql public class BitweenDbContext(DbContextOptions options, RequestContext requestContext, IPublish publish) : Bitween.BitweenDbContext(options, requestContext, publish) { - /// Backs ids — see the note in OnModelCreating. + /// Backs ids - see the note in OnModelCreating. public const string DocumentIdSequence = "DocumentIds"; protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -17,8 +17,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.UseSchedulerSqlServer(); // Documents.Id became database-generated after the table already existed. SQL Server - // cannot add IDENTITY to an existing column — EF refuses outright ("to change the - // IDENTITY property of a column, the column needs to be dropped and recreated") — and + // cannot add IDENTITY to an existing column - EF refuses outright ("to change the + // IDENTITY property of a column, the column needs to be dropped and recreated") - and // rebuilding Documents would mean dropping the foreign keys Subscriptions and Xchanges // hold against it. A sequence-backed default generates ids the same way with no table // rebuild. Postgres uses identity and MySQL AUTO_INCREMENT, which both alter in place. diff --git a/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs index 33fa07c5..dbeee0c1 100644 --- a/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs +++ b/SW.Bitween.MsSql/Migrations/20260726123818_AddRolesAndPermissions.cs @@ -80,7 +80,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // Every existing account keeps exactly the access it had: its coarse AccountRole // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything - // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // unrecognised lands on Viewer - least privilege. Idempotent, so re-running or // ordering against the seeded admin account can't produce duplicates. migrationBuilder.Sql(""" INSERT INTO [AccountRoles] ([AccountId], [RoleId]) diff --git a/SW.Bitween.MsSql/Migrations/20260907082347_MergeExternalBusWithAuditTrail.cs b/SW.Bitween.MsSql/Migrations/20260907082347_MergeExternalBusWithAuditTrail.cs index 146ad8e3..74f04109 100644 --- a/SW.Bitween.MsSql/Migrations/20260907082347_MergeExternalBusWithAuditTrail.cs +++ b/SW.Bitween.MsSql/Migrations/20260907082347_MergeExternalBusWithAuditTrail.cs @@ -12,14 +12,14 @@ public partial class MergeExternalBusWithAuditTrail : Migration /// /// Deliberately empty. /// - /// Two branches added migrations at the same time — the audit trail on releases/r10.0, the - /// external bus data sources here — so each side's model snapshot described only its own + /// Two branches added migrations at the same time - the audit trail on releases/r10.0, the + /// external bus data sources here - so each side's model snapshot described only its own /// half. A snapshot is generated from the model, and hand-merging generated code is how one /// silently stops matching it, so the merge took r10's snapshot wholesale and let EF /// regenerate from the combined model. /// /// Regenerating produces this migration, whose Up() would create the data sources, the - /// deduplication table and the cluster leases — all of which migrations already on this + /// deduplication table and the cluster leases - all of which migrations already on this /// branch create. Running it would fail on a fresh database and do nothing on an existing /// one. What is worth keeping is the snapshot beside it, which now describes both halves. /// MigrationDriftTests is what proves that, and it is the reason this is safe to leave diff --git a/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.cs b/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.cs index c01c418c..a4141f4a 100644 --- a/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.cs +++ b/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.cs @@ -22,7 +22,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // EF scaffolded a plain UpdateData here, because as far as the model is concerned the // seeded row simply gained a column value. That would have demanded a new password from // every installation on the next deploy, including the ones that chose theirs years ago - // — a forced interruption for people who had already done the right thing. + // - a forced interruption for people who had already done the right thing. // // Matching on the stored hash narrows it to the installations the flag is actually for: // those still signing in with the password published in our public repository. On diff --git a/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.Designer.cs new file mode 100644 index 00000000..017700f7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.Designer.cs @@ -0,0 +1,2462 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008091058_AddSubscriptionRunningSince")] + partial class AddSubscriptionRunningSince + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.cs b/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.cs new file mode 100644 index 00000000..4f5d96ea --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008091058_AddSubscriptionRunningSince.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddSubscriptionRunningSince : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RunningSince", + table: "Subscriptions", + type: "datetime2", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RunningSince", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.Designer.cs new file mode 100644 index 00000000..cc2ebe22 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.Designer.cs @@ -0,0 +1,2505 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008094929_AddOutboxMessages")] + partial class AddOutboxMessages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.cs b/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.cs new file mode 100644 index 00000000..fc6f7416 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008094929_AddOutboxMessages.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddOutboxMessages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + MessageType = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + Body = table.Column(type: "nvarchar(max)", nullable: false), + Headers = table.Column(type: "nvarchar(max)", nullable: true), + CreatedOn = table.Column(type: "datetime2", nullable: false), + PublishedOn = table.Column(type: "datetime2", nullable: true), + ClaimedUntil = table.Column(type: "datetime2", nullable: true), + Attempts = table.Column(type: "int", nullable: false), + LastError = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessages_PublishedOn", + table: "OutboxMessages", + column: "PublishedOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessages"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.Designer.cs new file mode 100644 index 00000000..a238fcbd --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.Designer.cs @@ -0,0 +1,2509 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008102242_AddPartnerKeyPrefix")] + partial class AddPartnerKeyPrefix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.cs b/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.cs new file mode 100644 index 00000000..be592c65 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008102242_AddPartnerKeyPrefix.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddPartnerKeyPrefix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "KeyPrefix", + table: "PartnerApiCredentials", + type: "nvarchar(10)", + maxLength: 10, + nullable: true); + + migrationBuilder.UpdateData( + table: "PartnerApiCredentials", + keyColumns: new[] { "Id", "PartnerId" }, + keyValues: new object[] { 1, 1 }, + column: "KeyPrefix", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "KeyPrefix", + table: "PartnerApiCredentials"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.Designer.cs new file mode 100644 index 00000000..dcd7fbd7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.Designer.cs @@ -0,0 +1,2512 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008114136_AddRefreshTokenSupersededOn")] + partial class AddRefreshTokenSupersededOn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.cs b/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.cs new file mode 100644 index 00000000..84551b47 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008114136_AddRefreshTokenSupersededOn.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddRefreshTokenSupersededOn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SupersededOn", + table: "RefreshTokens", + type: "datetime2", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SupersededOn", + table: "RefreshTokens"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.Designer.cs new file mode 100644 index 00000000..8a7449f5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.Designer.cs @@ -0,0 +1,2515 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008122734_AddAccountMicrosoftIdentity")] + partial class AddAccountMicrosoftIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.cs b/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.cs new file mode 100644 index 00000000..82cdf604 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008122734_AddAccountMicrosoftIdentity.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddAccountMicrosoftIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MicrosoftIdentity", + table: "Accounts", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "MicrosoftIdentity", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MicrosoftIdentity", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.Designer.cs new file mode 100644 index 00000000..f3aefebd --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.Designer.cs @@ -0,0 +1,2523 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008134936_RetryRunFailuresAndQuietNotifications")] + partial class RetryRunFailuresAndQuietNotifications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.cs b/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.cs new file mode 100644 index 00000000..11774182 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008134936_RetryRunFailuresAndQuietNotifications.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class RetryRunFailuresAndQuietNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Suppressed", + table: "XchangeNotifications", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "RunFailures", + table: "DelayedRetries", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_XchangeNotifications_NotifierId_FinishedOn", + table: "XchangeNotifications", + columns: new[] { "NotifierId", "FinishedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_XchangeNotifications_NotifierId_FinishedOn", + table: "XchangeNotifications"); + + migrationBuilder.DropColumn( + name: "Suppressed", + table: "XchangeNotifications"); + + migrationBuilder.DropColumn( + name: "RunFailures", + table: "DelayedRetries"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.Designer.cs new file mode 100644 index 00000000..e343c672 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.Designer.cs @@ -0,0 +1,2560 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008141140_AddIdempotencyKeys")] + partial class AddIdempotencyKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.cs b/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.cs new file mode 100644 index 00000000..100d8a46 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008141140_AddIdempotencyKeys.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddIdempotencyKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "IdempotencyKeys", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Scope = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Key = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + XchangeId = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IdempotencyKeys", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_IdempotencyKeys_CreatedOn", + table: "IdempotencyKeys", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_IdempotencyKeys_Scope_Key", + table: "IdempotencyKeys", + columns: new[] { "Scope", "Key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IdempotencyKeys"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.Designer.cs new file mode 100644 index 00000000..43e30acc --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.Designer.cs @@ -0,0 +1,2566 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008142226_AddSubscriptionAutoPause")] + partial class AddSubscriptionAutoPause + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.cs b/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.cs new file mode 100644 index 00000000..436666be --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008142226_AddSubscriptionAutoPause.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddSubscriptionAutoPause : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AutoPauseAfterFailures", + table: "Subscriptions", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "PausedAutomatically", + table: "Subscriptions", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AutoPauseAfterFailures", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "PausedAutomatically", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.Designer.cs new file mode 100644 index 00000000..6ad13eff --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.Designer.cs @@ -0,0 +1,2569 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008143200_AddDocumentValidationSchema")] + partial class AddDocumentValidationSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.cs b/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.cs new file mode 100644 index 00000000..44b4bf9d --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008143200_AddDocumentValidationSchema.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddDocumentValidationSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ValidationSchema", + table: "Documents", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.UpdateData( + table: "Documents", + keyColumn: "Id", + keyValue: 10001, + column: "ValidationSchema", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ValidationSchema", + table: "Documents"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.Designer.cs b/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.Designer.cs new file mode 100644 index 00000000..7d72ed55 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.Designer.cs @@ -0,0 +1,2589 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008170030_AddSubscriptionAdapterVersions")] + partial class AddSubscriptionAdapterVersions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.cs b/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.cs new file mode 100644 index 00000000..5886b11c --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261008170030_AddSubscriptionAdapterVersions.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddSubscriptionAdapterVersions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HandlerVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "MapperVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReceiverVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "ValidatorVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HandlerVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "MapperVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "ReceiverVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "ValidatorVersion", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.Designer.cs b/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.Designer.cs new file mode 100644 index 00000000..01c2651a --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.Designer.cs @@ -0,0 +1,2627 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009082812_AdapterSourceAccess")] + partial class AdapterSourceAccess + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.cs b/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.cs new file mode 100644 index 00000000..2c372b5e --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261009082812_AdapterSourceAccess.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AdapterSourceAccess : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AdapterSourceAccesses", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true), + Path = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + AccountId = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true), + OccurredOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterSourceAccesses", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AdapterSourceAccesses_AdapterId_OccurredOn", + table: "AdapterSourceAccesses", + columns: new[] { "AdapterId", "OccurredOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterSourceAccesses"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.Designer.cs b/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.Designer.cs new file mode 100644 index 00000000..b80cb328 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.Designer.cs @@ -0,0 +1,2732 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009141401_AdapterDrafts")] + partial class AdapterDrafts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.cs b/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.cs new file mode 100644 index 00000000..e1fff62b --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261009141401_AdapterDrafts.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AdapterDrafts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AdapterDrafts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Language = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + Kind = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: true), + BaseVersion = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true), + FilesJson = table.Column(type: "nvarchar(max)", nullable: false), + FilesHash = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + ModifiedOn = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterDrafts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AdapterReleases", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + Action = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + DraftId = table.Column(type: "int", nullable: true), + AccountId = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true), + OccurredOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterReleases", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AdapterDrafts_AdapterId", + table: "AdapterDrafts", + column: "AdapterId"); + + migrationBuilder.CreateIndex( + name: "IX_AdapterReleases_AdapterId_OccurredOn", + table: "AdapterReleases", + columns: new[] { "AdapterId", "OccurredOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterDrafts"); + + migrationBuilder.DropTable( + name: "AdapterReleases"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.Designer.cs b/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.Designer.cs new file mode 100644 index 00000000..1bda8042 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.Designer.cs @@ -0,0 +1,2770 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010082907_ClusterNodes")] + partial class ClusterNodes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.cs b/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.cs new file mode 100644 index 00000000..91db8584 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010082907_ClusterNodes.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ClusterNodes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClusterNodes", + columns: table => new + { + Id = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Host = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true), + StartedOn = table.Column(type: "datetime2", nullable: false), + LastSeenOn = table.Column(type: "datetime2", nullable: false), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true), + DataSources = table.Column(type: "bit", nullable: false), + Runtimes = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ClusterNodes", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClusterNodes_LastSeenOn", + table: "ClusterNodes", + column: "LastSeenOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClusterNodes"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.Designer.cs b/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.Designer.cs new file mode 100644 index 00000000..288604f5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.Designer.cs @@ -0,0 +1,2787 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010084341_ApiKeyUses")] + partial class ApiKeyUses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.cs b/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.cs new file mode 100644 index 00000000..911bf2ac --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010084341_ApiKeyUses.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ApiKeyUses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeyUses", + columns: table => new + { + PartnerId = table.Column(type: "int", nullable: false), + KeyName = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + LastUsedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeyUses", x => new { x.PartnerId, x.KeyName }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeyUses"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.Designer.cs b/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.Designer.cs new file mode 100644 index 00000000..45e68552 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.Designer.cs @@ -0,0 +1,2825 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010085314_OperatorActionsAndLastSignIn")] + partial class OperatorActionsAndLastSignIn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.cs b/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.cs new file mode 100644 index 00000000..308229cc --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261010085314_OperatorActionsAndLastSignIn.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class OperatorActionsAndLastSignIn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastSignInOn", + table: "Accounts", + type: "datetime2", + nullable: true); + + migrationBuilder.CreateTable( + name: "OperatorActions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Action = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Target = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Detail = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + AccountId = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + OccurredOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OperatorActions", x => x.Id); + }); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "LastSignInOn", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_OperatorActions_OccurredOn", + table: "OperatorActions", + column: "OccurredOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OperatorActions"); + + migrationBuilder.DropColumn( + name: "LastSignInOn", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.Designer.cs b/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.Designer.cs new file mode 100644 index 00000000..d1926456 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.Designer.cs @@ -0,0 +1,3091 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011085211_Metrics")] + partial class Metrics + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime2"); + + b.Property("AckRate") + .HasColumnType("float"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("float"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.cs b/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.cs new file mode 100644 index 00000000..8d02d9de --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261011085211_Metrics.cs @@ -0,0 +1,193 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class Metrics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DurationMs", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "HandlerVersion", + table: "XchangeResults", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "MapperVersion", + table: "XchangeResults", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "NodeName", + table: "XchangeResults", + type: "varchar(100)", + unicode: false, + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "ResidentHealth", + table: "ClusterNodes", + type: "nvarchar(4000)", + maxLength: 4000, + nullable: true); + + migrationBuilder.CreateTable( + name: "AdapterStats", + columns: table => new + { + Hour = table.Column(type: "datetime2", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + Role = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + Runtime = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + Calls = table.Column(type: "int", nullable: false), + Failures = table.Column(type: "int", nullable: false), + Timeouts = table.Column(type: "int", nullable: false), + LimitStops = table.Column(type: "int", nullable: false), + Crashes = table.Column(type: "int", nullable: false), + DurationSumMs = table.Column(type: "bigint", nullable: false), + DurationMaxMs = table.Column(type: "bigint", nullable: false), + D0 = table.Column(type: "int", nullable: false), + D1 = table.Column(type: "int", nullable: false), + D2 = table.Column(type: "int", nullable: false), + D3 = table.Column(type: "int", nullable: false), + D4 = table.Column(type: "int", nullable: false), + D5 = table.Column(type: "int", nullable: false), + D6 = table.Column(type: "int", nullable: false), + D7 = table.Column(type: "int", nullable: false), + D8 = table.Column(type: "int", nullable: false), + D9 = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterStats", x => new { x.Hour, x.Node, x.AdapterId, x.Version, x.Role, x.Runtime }); + }); + + migrationBuilder.CreateTable( + name: "ErrorStats", + columns: table => new + { + Hour = table.Column(type: "datetime2", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false), + Scope = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + ScopeKey = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false), + Fingerprint = table.Column(type: "varchar(16)", unicode: false, maxLength: 16, nullable: false), + Sample = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Count = table.Column(type: "int", nullable: false), + LastSeenOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ErrorStats", x => new { x.Hour, x.Node, x.Scope, x.ScopeKey, x.Fingerprint }); + }); + + migrationBuilder.CreateTable( + name: "ExchangeStats", + columns: table => new + { + Hour = table.Column(type: "datetime2", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false), + SubscriptionId = table.Column(type: "int", nullable: false), + PartnerId = table.Column(type: "int", nullable: false), + Total = table.Column(type: "int", nullable: false), + Succeeded = table.Column(type: "int", nullable: false), + BadResponse = table.Column(type: "int", nullable: false), + Failed = table.Column(type: "int", nullable: false), + FailedAtMapper = table.Column(type: "int", nullable: false), + FailedAtHandler = table.Column(type: "int", nullable: false), + Retries = table.Column(type: "int", nullable: false), + ManualRetries = table.Column(type: "int", nullable: false), + BytesIn = table.Column(type: "bigint", nullable: false), + BytesOut = table.Column(type: "bigint", nullable: false), + DurationSumMs = table.Column(type: "bigint", nullable: false), + DurationMaxMs = table.Column(type: "bigint", nullable: false), + D0 = table.Column(type: "int", nullable: false), + D1 = table.Column(type: "int", nullable: false), + D2 = table.Column(type: "int", nullable: false), + D3 = table.Column(type: "int", nullable: false), + D4 = table.Column(type: "int", nullable: false), + D5 = table.Column(type: "int", nullable: false), + D6 = table.Column(type: "int", nullable: false), + D7 = table.Column(type: "int", nullable: false), + D8 = table.Column(type: "int", nullable: false), + D9 = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExchangeStats", x => new { x.Hour, x.Node, x.SubscriptionId, x.PartnerId }); + }); + + migrationBuilder.CreateTable( + name: "QueueSnapshots", + columns: table => new + { + At = table.Column(type: "datetime2", nullable: false), + QueueDepth = table.Column(type: "bigint", nullable: false), + RetryBacklog = table.Column(type: "bigint", nullable: false), + DeadLetterBacklog = table.Column(type: "bigint", nullable: false), + IncomingRate = table.Column(type: "float", nullable: false), + AckRate = table.Column(type: "float", nullable: false), + Consumers = table.Column(type: "int", nullable: false), + UnhealthyConsumers = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QueueSnapshots", x => x.At); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterStats"); + + migrationBuilder.DropTable( + name: "ErrorStats"); + + migrationBuilder.DropTable( + name: "ExchangeStats"); + + migrationBuilder.DropTable( + name: "QueueSnapshots"); + + migrationBuilder.DropColumn( + name: "DurationMs", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "HandlerVersion", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "MapperVersion", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "NodeName", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "ResidentHealth", + table: "ClusterNodes"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.Designer.cs b/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.Designer.cs new file mode 100644 index 00000000..8d224563 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.Designer.cs @@ -0,0 +1,3172 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011104346_ServiceAccounts")] + partial class ServiceAccounts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ExpiresOn") + .HasColumnType("datetime2"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime2"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime2"); + + b.Property("AckRate") + .HasColumnType("float"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("float"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.cs b/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.cs new file mode 100644 index 00000000..a36e6278 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261011104346_ServiceAccounts.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ServiceAccounts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Kind", + table: "Accounts", + type: "tinyint", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.CreateTable( + name: "ServiceTokens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AccountId = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Hash = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: false), + Shown = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + ExpiresOn = table.Column(type: "datetime2", nullable: false), + RevokedOn = table.Column(type: "datetime2", nullable: true), + RevokedBy = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + LastUsedOn = table.Column(type: "datetime2", nullable: true), + LastUsedFrom = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: true), + CreatedOn = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + ModifiedOn = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceTokens", x => x.Id); + table.ForeignKey( + name: "FK_ServiceTokens_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "Kind", + value: (byte)0); + + migrationBuilder.CreateIndex( + name: "IX_ServiceTokens_AccountId", + table: "ServiceTokens", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceTokens_Hash", + table: "ServiceTokens", + column: "Hash", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ServiceTokens"); + + migrationBuilder.DropColumn( + name: "Kind", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.Designer.cs b/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.Designer.cs new file mode 100644 index 00000000..6daf4a88 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.Designer.cs @@ -0,0 +1,3175 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012094158_GatewayFailureAnswer")] + partial class GatewayFailureAnswer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ExpiresOn") + .HasColumnType("datetime2"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime2"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FailureAnswer") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime2"); + + b.Property("AckRate") + .HasColumnType("float"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("float"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.cs b/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.cs new file mode 100644 index 00000000..6e1b3425 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261012094158_GatewayFailureAnswer.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class GatewayFailureAnswer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailureAnswer", + table: "ApiGateways", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailureAnswer", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.Designer.cs b/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.Designer.cs new file mode 100644 index 00000000..ef83ea5a --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.Designer.cs @@ -0,0 +1,3181 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012095632_ExpectTraffic")] + partial class ExpectTraffic + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("MustChangePassword") + .HasColumnType("bit"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.Property("SupersededOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ExpiresOn") + .HasColumnType("datetime2"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime2"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime2"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("float"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime2"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime2"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("RetiredOn") + .HasColumnType("datetime2"); + + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FailureAnswer") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime2"); + + b.Property("AckRate") + .HasColumnType("float"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("float"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasFilter("[LoginIdentity] IS NOT NULL"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ExpectTraffic") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("bit"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("QuietSince") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("bit"); + + b.Property("RunningSince") + .HasColumnType("datetime2"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("nvarchar(max)"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("Suppressed") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.cs b/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.cs new file mode 100644 index 00000000..17b2956e --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20261012095632_ExpectTraffic.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ExpectTraffic : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExpectTraffic", + table: "Subscriptions", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "QuietSince", + table: "Subscriptions", + type: "datetime2", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExpectTraffic", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "QuietSince", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs index 71ecd959..5c202341 100644 --- a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -60,12 +60,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FailedLoginCount") .HasColumnType("int"); + b.Property("Kind") + .HasColumnType("tinyint"); + + b.Property("LastSignInOn") + .HasColumnType("datetime2"); + b.Property("LockoutEnd") .HasColumnType("datetime2"); b.Property("LoginMethods") .HasColumnType("tinyint"); + b.Property("MicrosoftIdentity") + .HasColumnType("nvarchar(max)"); + b.Property("ModifiedBy") .HasColumnType("nvarchar(max)"); @@ -102,6 +111,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Email = "admin@Bitween.systems", EmailProvider = (byte)0, FailedLoginCount = 0, + Kind = (byte)0, LoginMethods = (byte)2, MustChangePassword = true, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", @@ -140,6 +150,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LoginMethod") .HasColumnType("tinyint"); + b.Property("SupersededOn") + .HasColumnType("datetime2"); + b.HasKey("Id"); b.HasIndex("AccountId"); @@ -219,6 +232,234 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ExpiresOn") + .HasColumnType("datetime2"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime2"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime2"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => { b.Property("Id") @@ -295,6 +536,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ClusterLeases", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("bit"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => { b.Property("AdapterId") @@ -512,6 +795,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("On") .HasColumnType("datetime2"); + b.Property("RunFailures") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("On"); @@ -562,6 +848,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RetiredOn") .HasColumnType("datetime2"); + b.Property("ValidationSchema") + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.HasIndex("BusMessageTypeName") @@ -606,6 +895,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime2"); + b.Property("FailureAnswer") + .HasColumnType("int"); + b.Property("Inactive") .HasColumnType("bit"); @@ -654,146 +946,427 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ApiGatewayId") .HasColumnType("int"); - b.Property("PartnerId") + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SecretProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") .HasColumnType("int"); - b.Property("SubscriptionId") + b.Property("D8") .HasColumnType("int"); - b.Property("CreatedBy") - .HasColumnType("nvarchar(max)"); + b.Property("D9") + .HasColumnType("int"); - b.Property("CreatedOn") - .HasColumnType("datetime2"); + b.Property("DurationMaxMs") + .HasColumnType("bigint"); - b.Property("ModifiedBy") - .HasColumnType("nvarchar(max)"); + b.Property("DurationSumMs") + .HasColumnType("bigint"); - b.Property("ModifiedOn") - .HasColumnType("datetime2"); + b.Property("Failures") + .HasColumnType("int"); - b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + b.Property("LimitStops") + .HasColumnType("int"); - b.HasIndex("PartnerId"); + b.Property("Timeouts") + .HasColumnType("int"); - b.HasIndex("SubscriptionId"); + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); - b.ToTable("ApiGatewayPartners", (string)null); + b.ToTable("AdapterStats", (string)null); }); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); + b.Property("Hour") + .HasColumnType("datetime2"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); - b.Property("CreatedBy") - .HasColumnType("nvarchar(max)"); + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); - b.Property("CreatedOn") - .HasColumnType("datetime2"); + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); - b.Property("DataSourceId") - .HasColumnType("int"); + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); - b.Property("DocumentId") + b.Property("Count") .HasColumnType("int"); - b.Property("Endpoint") + b.Property("LastSeenOn") + .HasColumnType("datetime2"); + + b.Property("Sample") .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime2"); + + b.Property("Node") + .HasMaxLength(100) .IsUnicode(false) - .HasColumnType("varchar(500)"); + .HasColumnType("varchar(100)"); - b.Property("EndpointProperties") - .HasColumnType("nvarchar(max)"); + b.Property("SubscriptionId") + .HasColumnType("int"); - b.Property("Inactive") - .HasColumnType("bit"); + b.Property("PartnerId") + .HasColumnType("int"); - b.Property("ModifiedBy") - .HasColumnType("nvarchar(max)"); + b.Property("BadResponse") + .HasColumnType("int"); - b.Property("ModifiedOn") - .HasColumnType("datetime2"); + b.Property("BytesIn") + .HasColumnType("bigint"); - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("nvarchar(200)"); + b.Property("BytesOut") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.Property("D0") + .HasColumnType("int"); - b.HasIndex("DataSourceId"); + b.Property("D1") + .HasColumnType("int"); - b.HasIndex("DocumentId"); + b.Property("D2") + .HasColumnType("int"); - b.ToTable("BusGateways", (string)null); - }); + b.Property("D3") + .HasColumnType("int"); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("D4") .HasColumnType("int"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("D5") + .HasColumnType("int"); - b.Property("BusGatewayId") + b.Property("D6") .HasColumnType("int"); - b.Property("CreatedBy") - .HasColumnType("nvarchar(max)"); + b.Property("D7") + .HasColumnType("int"); - b.Property("CreatedOn") - .HasColumnType("datetime2"); + b.Property("D8") + .HasColumnType("int"); - b.Property("MatchExpression") - .HasColumnType("nvarchar(max)"); + b.Property("D9") + .HasColumnType("int"); - b.Property("ModifiedBy") - .HasColumnType("nvarchar(max)"); + b.Property("DurationMaxMs") + .HasColumnType("bigint"); - b.Property("ModifiedOn") - .HasColumnType("datetime2"); + b.Property("DurationSumMs") + .HasColumnType("bigint"); - b.Property("PartnerId") + b.Property("Failed") .HasColumnType("int"); - b.Property("SubscriptionId") + b.Property("FailedAtHandler") .HasColumnType("int"); - b.HasKey("Id"); + b.Property("FailedAtMapper") + .HasColumnType("int"); - b.HasIndex("BusGatewayId"); + b.Property("ManualRetries") + .HasColumnType("int"); - b.HasIndex("PartnerId"); + b.Property("Retries") + .HasColumnType("int"); - b.HasIndex("SubscriptionId"); + b.Property("Succeeded") + .HasColumnType("int"); - b.ToTable("BusGatewayRoutes", (string)null); + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); }); - modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => { - b.Property("Id") - .HasMaxLength(200) - .IsUnicode(false) - .HasColumnType("varchar(200)"); + b.Property("At") + .HasColumnType("datetime2"); - b.Property("Name") - .HasColumnType("nvarchar(max)"); + b.Property("AckRate") + .HasColumnType("float"); - b.Property("SecretProperties") - .HasColumnType("nvarchar(max)"); + b.Property("Consumers") + .HasColumnType("int"); - b.Property("Values") - .HasColumnType("nvarchar(max)"); + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.Property("IncomingRate") + .HasColumnType("float"); - b.ToTable("GlobalAdapterValuesSets", (string)null); + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); }); modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => @@ -882,6 +1455,84 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OnHoldXchanges", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime2"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Headers") + .HasColumnType("nvarchar(max)"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => { b.Property("Id") @@ -1087,6 +1738,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregationTarget") .HasColumnType("tinyint"); + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + b.Property("CategoryId") .HasColumnType("int"); @@ -1105,6 +1759,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("ExpectTraffic") + .HasColumnType("nvarchar(max)"); + b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -1113,6 +1770,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HandlerProperties") .HasColumnType("nvarchar(max)"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("Inactive") .HasColumnType("bit"); @@ -1130,6 +1792,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MapperProperties") .HasColumnType("nvarchar(max)"); + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("MatchExpression") .HasColumnType("nvarchar(max)"); @@ -1141,9 +1808,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PartnerId") .HasColumnType("int"); + b.Property("PausedAutomatically") + .HasColumnType("bit"); + b.Property("PausedOn") .HasColumnType("datetime2"); + b.Property("QuietSince") + .HasColumnType("datetime2"); + b.Property("ReceiveOn") .HasColumnType("datetime2"); @@ -1155,6 +1828,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ReceiverProperties") .HasColumnType("nvarchar(max)"); + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("ResponseMessageTypeName") .HasMaxLength(500) .IsUnicode(false) @@ -1169,6 +1847,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RunOnBadResponses") .HasColumnType("bit"); + b.Property("RunningSince") + .HasColumnType("datetime2"); + b.Property("Temporary") .HasColumnType("bit"); @@ -1183,6 +1864,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ValidatorProperties") .HasColumnType("nvarchar(max)"); + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("WorkGroupId") .HasColumnType("int"); @@ -1433,6 +2119,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Success") .HasColumnType("bit"); + b.Property("Suppressed") + .HasColumnType("bit"); + b.Property("XchangeId") .HasMaxLength(50) .IsUnicode(false) @@ -1442,6 +2131,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("XchangeId"); + b.HasIndex("NotifierId", "FinishedOn"); + b.ToTable("XchangeNotifications", (string)null); }); @@ -1478,12 +2169,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AttemptNumber") .HasColumnType("int"); + b.Property("DurationMs") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("nvarchar(max)"); b.Property("FinishedOn") .HasColumnType("datetime2"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + b.Property("OutputBad") .HasColumnType("bit"); @@ -2097,6 +2806,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => { b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") @@ -2210,6 +2928,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + b1.Property("Name") .IsRequired() .HasMaxLength(500) diff --git a/SW.Bitween.MySql/Migrations/20201112230621_Update5.cs b/SW.Bitween.MySql/Migrations/20201112230621_Update5.cs index d08247d4..4fae03e8 100644 --- a/SW.Bitween.MySql/Migrations/20201112230621_Update5.cs +++ b/SW.Bitween.MySql/Migrations/20201112230621_Update5.cs @@ -83,15 +83,18 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id", onDelete: ReferentialAction.Restrict); + // Backticks: MySQL reads double quotes as string literals unless the server runs in + // ANSI_QUOTES mode, so a fresh install on a default server stopped at this migration. + // An install that already applied it is not affected - applied migrations never rerun. migrationBuilder.Sql(@" - CREATE TABLE ""SubscriptionSchedules"" ( - ""Id"" int NOT NULL AUTO_INCREMENT, - ""Recurrence"" tinyint unsigned NOT NULL, - ""On"" bigint NOT NULL, - ""Backwards"" tinyint(1) NOT NULL, - ""SubscriptionId"" int NOT NULL, - PRIMARY KEY (""Id"",""SubscriptionId""), - CONSTRAINT ""FK_SubscriptionSchedules_Subscriptions_SubscriptionId"" FOREIGN KEY (""SubscriptionId"") REFERENCES ""Subscriptions"" (""Id"") ON DELETE CASCADE); + CREATE TABLE `SubscriptionSchedules` ( + `Id` int NOT NULL AUTO_INCREMENT, + `Recurrence` tinyint unsigned NOT NULL, + `On` bigint NOT NULL, + `Backwards` tinyint(1) NOT NULL, + `SubscriptionId` int NOT NULL, + PRIMARY KEY (`Id`,`SubscriptionId`), + CONSTRAINT `FK_SubscriptionSchedules_Subscriptions_SubscriptionId` FOREIGN KEY (`SubscriptionId`) REFERENCES `Subscriptions` (`Id`) ON DELETE CASCADE); "); diff --git a/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs index c32df956..70c35ccc 100644 --- a/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs +++ b/SW.Bitween.MySql/Migrations/20260726123628_AddDocumentCodeAndAutoId.cs @@ -11,6 +11,11 @@ public partial class AddDocumentCodeAndAutoId : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { + // MySQL 8 refuses to modify a column other tables' foreign keys reference ("Cannot + // change column 'Id': used in a foreign key constraint") while the checks are on, so + // this stopped every MySQL install, new or upgrading. Making the key auto-increment + // changes no value any foreign key relies on; the checks are back on straight after. + migrationBuilder.Sql("SET FOREIGN_KEY_CHECKS = 0;"); migrationBuilder.AlterColumn( name: "Id", table: "Documents", @@ -19,6 +24,7 @@ protected override void Up(MigrationBuilder migrationBuilder) oldClrType: typeof(int), oldType: "int") .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + migrationBuilder.Sql("SET FOREIGN_KEY_CHECKS = 1;"); migrationBuilder.AddColumn( name: "Code", diff --git a/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs index 7b5ba487..76812aea 100644 --- a/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs +++ b/SW.Bitween.MySql/Migrations/20260726123824_AddRolesAndPermissions.cs @@ -88,7 +88,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // Every existing account keeps exactly the access it had: its coarse AccountRole // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything - // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // unrecognised lands on Viewer - least privilege. Idempotent, so re-running or // ordering against the seeded admin account can't produce duplicates. migrationBuilder.Sql(""" INSERT INTO `AccountRoles` (`AccountId`, `RoleId`) diff --git a/SW.Bitween.MySql/Migrations/20260907082342_MergeExternalBusWithAuditTrail.cs b/SW.Bitween.MySql/Migrations/20260907082342_MergeExternalBusWithAuditTrail.cs index 67c722b5..ad14adbe 100644 --- a/SW.Bitween.MySql/Migrations/20260907082342_MergeExternalBusWithAuditTrail.cs +++ b/SW.Bitween.MySql/Migrations/20260907082342_MergeExternalBusWithAuditTrail.cs @@ -13,14 +13,14 @@ public partial class MergeExternalBusWithAuditTrail : Migration /// /// Deliberately empty. /// - /// Two branches added migrations at the same time — the audit trail on releases/r10.0, the - /// external bus data sources here — so each side's model snapshot described only its own + /// Two branches added migrations at the same time - the audit trail on releases/r10.0, the + /// external bus data sources here - so each side's model snapshot described only its own /// half. A snapshot is generated from the model, and hand-merging generated code is how one /// silently stops matching it, so the merge took r10's snapshot wholesale and let EF /// regenerate from the combined model. /// /// Regenerating produces this migration, whose Up() would create the data sources, the - /// deduplication table and the cluster leases — all of which migrations already on this + /// deduplication table and the cluster leases - all of which migrations already on this /// branch create. Running it would fail on a fresh database and do nothing on an existing /// one. What is worth keeping is the snapshot beside it, which now describes both halves. /// MigrationDriftTests is what proves that, and it is the reason this is safe to leave diff --git a/SW.Bitween.MySql/Migrations/20260915090329_AddMustChangePassword.cs b/SW.Bitween.MySql/Migrations/20260915090329_AddMustChangePassword.cs index a8e7326a..aadc6f64 100644 --- a/SW.Bitween.MySql/Migrations/20260915090329_AddMustChangePassword.cs +++ b/SW.Bitween.MySql/Migrations/20260915090329_AddMustChangePassword.cs @@ -22,7 +22,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // EF scaffolded a plain UpdateData here, because as far as the model is concerned the // seeded row simply gained a column value. That would have demanded a new password from // every installation on the next deploy, including the ones that chose theirs years ago - // — a forced interruption for people who had already done the right thing. + // - a forced interruption for people who had already done the right thing. // // Matching on the stored hash narrows it to the installations the flag is actually for: // those still signing in with the password published in our public repository. On diff --git a/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.Designer.cs b/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.Designer.cs new file mode 100644 index 00000000..ea8b8ba0 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.Designer.cs @@ -0,0 +1,2454 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008091055_AddSubscriptionRunningSince")] + partial class AddSubscriptionRunningSince + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.cs b/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.cs new file mode 100644 index 00000000..831d8c7e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008091055_AddSubscriptionRunningSince.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddSubscriptionRunningSince : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RunningSince", + table: "Subscriptions", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RunningSince", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.Designer.cs b/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.Designer.cs new file mode 100644 index 00000000..f19f09a8 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.Designer.cs @@ -0,0 +1,2497 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008094921_AddOutboxMessages")] + partial class AddOutboxMessages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.cs b/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.cs new file mode 100644 index 00000000..51c4fd55 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008094921_AddOutboxMessages.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddOutboxMessages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + MessageType = table.Column(type: "varchar(500)", maxLength: 500, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Body = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Headers = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + PublishedOn = table.Column(type: "datetime(6)", nullable: true), + ClaimedUntil = table.Column(type: "datetime(6)", nullable: true), + Attempts = table.Column(type: "int", nullable: false), + LastError = table.Column(type: "varchar(2000)", maxLength: 2000, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessages_PublishedOn", + table: "OutboxMessages", + column: "PublishedOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessages"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.Designer.cs b/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.Designer.cs new file mode 100644 index 00000000..68bdb086 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.Designer.cs @@ -0,0 +1,2501 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008102239_AddPartnerKeyPrefix")] + partial class AddPartnerKeyPrefix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.cs b/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.cs new file mode 100644 index 00000000..f4d0ca71 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008102239_AddPartnerKeyPrefix.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddPartnerKeyPrefix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "KeyPrefix", + table: "PartnerApiCredentials", + type: "varchar(10)", + maxLength: 10, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "PartnerApiCredentials", + keyColumns: new[] { "Id", "PartnerId" }, + keyValues: new object[] { 1, 1 }, + column: "KeyPrefix", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "KeyPrefix", + table: "PartnerApiCredentials"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.Designer.cs b/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.Designer.cs new file mode 100644 index 00000000..dc4f7470 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.Designer.cs @@ -0,0 +1,2504 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008114133_AddRefreshTokenSupersededOn")] + partial class AddRefreshTokenSupersededOn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.cs b/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.cs new file mode 100644 index 00000000..6dc0f9ac --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008114133_AddRefreshTokenSupersededOn.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddRefreshTokenSupersededOn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SupersededOn", + table: "RefreshTokens", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SupersededOn", + table: "RefreshTokens"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.Designer.cs b/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.Designer.cs new file mode 100644 index 00000000..41a90b06 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.Designer.cs @@ -0,0 +1,2507 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008122728_AddAccountMicrosoftIdentity")] + partial class AddAccountMicrosoftIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.cs b/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.cs new file mode 100644 index 00000000..a223d7bd --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008122728_AddAccountMicrosoftIdentity.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddAccountMicrosoftIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MicrosoftIdentity", + table: "Accounts", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "MicrosoftIdentity", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MicrosoftIdentity", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.Designer.cs b/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.Designer.cs new file mode 100644 index 00000000..9e4626f7 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.Designer.cs @@ -0,0 +1,2515 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008134933_RetryRunFailuresAndQuietNotifications")] + partial class RetryRunFailuresAndQuietNotifications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.cs b/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.cs new file mode 100644 index 00000000..cb71548d --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008134933_RetryRunFailuresAndQuietNotifications.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class RetryRunFailuresAndQuietNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Suppressed", + table: "XchangeNotifications", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "RunFailures", + table: "DelayedRetries", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_XchangeNotifications_NotifierId_FinishedOn", + table: "XchangeNotifications", + columns: new[] { "NotifierId", "FinishedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_XchangeNotifications_NotifierId_FinishedOn", + table: "XchangeNotifications"); + + migrationBuilder.DropColumn( + name: "Suppressed", + table: "XchangeNotifications"); + + migrationBuilder.DropColumn( + name: "RunFailures", + table: "DelayedRetries"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.Designer.cs b/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.Designer.cs new file mode 100644 index 00000000..fc6a2399 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.Designer.cs @@ -0,0 +1,2552 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008141137_AddIdempotencyKeys")] + partial class AddIdempotencyKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.cs b/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.cs new file mode 100644 index 00000000..575b127e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008141137_AddIdempotencyKeys.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddIdempotencyKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "IdempotencyKeys", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Scope = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Key = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + XchangeId = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IdempotencyKeys", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_IdempotencyKeys_CreatedOn", + table: "IdempotencyKeys", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_IdempotencyKeys_Scope_Key", + table: "IdempotencyKeys", + columns: new[] { "Scope", "Key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IdempotencyKeys"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.Designer.cs b/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.Designer.cs new file mode 100644 index 00000000..7535e24e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.Designer.cs @@ -0,0 +1,2558 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008142223_AddSubscriptionAutoPause")] + partial class AddSubscriptionAutoPause + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.cs b/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.cs new file mode 100644 index 00000000..fdbfdf24 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008142223_AddSubscriptionAutoPause.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddSubscriptionAutoPause : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AutoPauseAfterFailures", + table: "Subscriptions", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "PausedAutomatically", + table: "Subscriptions", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AutoPauseAfterFailures", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "PausedAutomatically", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.Designer.cs b/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.Designer.cs new file mode 100644 index 00000000..767eb4ee --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.Designer.cs @@ -0,0 +1,2561 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008143157_AddDocumentValidationSchema")] + partial class AddDocumentValidationSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.cs b/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.cs new file mode 100644 index 00000000..a4835b13 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008143157_AddDocumentValidationSchema.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddDocumentValidationSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ValidationSchema", + table: "Documents", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Documents", + keyColumn: "Id", + keyValue: 10001, + column: "ValidationSchema", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ValidationSchema", + table: "Documents"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.Designer.cs b/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.Designer.cs new file mode 100644 index 00000000..8945c92e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.Designer.cs @@ -0,0 +1,2581 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008170026_AddSubscriptionAdapterVersions")] + partial class AddSubscriptionAdapterVersions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.cs b/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.cs new file mode 100644 index 00000000..587476dc --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008170026_AddSubscriptionAdapterVersions.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddSubscriptionAdapterVersions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HandlerVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "MapperVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ReceiverVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ValidatorVersion", + table: "Subscriptions", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HandlerVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "MapperVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "ReceiverVersion", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "ValidatorVersion", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.Designer.cs b/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.Designer.cs new file mode 100644 index 00000000..ebf9c415 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.Designer.cs @@ -0,0 +1,2581 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008234945_QuartzTableNamesForCaseSensitiveServers")] + partial class QuartzTableNamesForCaseSensitiveServers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.cs b/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.cs new file mode 100644 index 00000000..f0a69411 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261008234945_QuartzTableNamesForCaseSensitiveServers.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + /// Gives the Quartz tables the names Quartz looks for, on a server where the case matters. + /// + /// + /// QuartzAndAutoRetry created them as SW.Scheduler's EF model names them - QRTZ_job_details + /// - while its Quartz store queries QRTZ_JOB_DETAILS. MySQL on Linux compares table names + /// case-sensitively by default (lower_case_table_names = 0), so the scheduler refused to start + /// there. On a server that ignores case the names already match, and renaming a table to what + /// it already is called would fail, so only a case-sensitive server renames, and only a table + /// still under the old name. Bitween reaches these tables through Quartz alone, never through + /// the EF model, so the model keeps SW.Scheduler's names. + /// + public partial class QuartzTableNamesForCaseSensitiveServers : Migration + { + private static readonly string[] Tables = + [ + "job_details", "triggers", "simple_triggers", "simprop_triggers", "cron_triggers", + "blob_triggers", "calendars", "paused_trigger_grps", "fired_triggers", "scheduler_state", "locks", + ]; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + foreach (var table in Tables) + Rename(migrationBuilder, $"QRTZ_{table}", $"QRTZ_{table.ToUpperInvariant()}"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + foreach (var table in Tables) + Rename(migrationBuilder, $"QRTZ_{table.ToUpperInvariant()}", $"QRTZ_{table}"); + } + + private static void Rename(MigrationBuilder migrationBuilder, string from, string to) => + migrationBuilder.Sql($""" + SET @rename = IF(@@lower_case_table_names = 0 AND EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND BINARY table_name = '{from}'), + 'RENAME TABLE `{from}` TO `{to}`', 'DO 0'); + PREPARE rename_statement FROM @rename; + EXECUTE rename_statement; + DEALLOCATE PREPARE rename_statement; + """); + } +} diff --git a/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.Designer.cs b/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.Designer.cs new file mode 100644 index 00000000..9d01a80d --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.Designer.cs @@ -0,0 +1,2619 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009082809_AdapterSourceAccess")] + partial class AdapterSourceAccess + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.cs b/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.cs new file mode 100644 index 00000000..441c8725 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261009082809_AdapterSourceAccess.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AdapterSourceAccess : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AdapterSourceAccesses", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Path = table.Column(type: "varchar(500)", maxLength: 500, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + AccountId = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + OccurredOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterSourceAccesses", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AdapterSourceAccesses_AdapterId_OccurredOn", + table: "AdapterSourceAccesses", + columns: new[] { "AdapterId", "OccurredOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterSourceAccesses"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.Designer.cs b/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.Designer.cs new file mode 100644 index 00000000..096f4c6f --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.Designer.cs @@ -0,0 +1,2724 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009141356_AdapterDrafts")] + partial class AdapterDrafts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.cs b/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.cs new file mode 100644 index 00000000..dc1d1f82 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261009141356_AdapterDrafts.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AdapterDrafts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AdapterDrafts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Language = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Kind = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + BaseVersion = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + FilesJson = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FilesHash = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "varchar(200)", maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedOn = table.Column(type: "datetime(6)", nullable: true), + ModifiedBy = table.Column(type: "varchar(200)", maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterDrafts", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AdapterReleases", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Action = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + DraftId = table.Column(type: "int", nullable: true), + AccountId = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + OccurredOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterReleases", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AdapterDrafts_AdapterId", + table: "AdapterDrafts", + column: "AdapterId"); + + migrationBuilder.CreateIndex( + name: "IX_AdapterReleases_AdapterId_OccurredOn", + table: "AdapterReleases", + columns: new[] { "AdapterId", "OccurredOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterDrafts"); + + migrationBuilder.DropTable( + name: "AdapterReleases"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.Designer.cs b/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.Designer.cs new file mode 100644 index 00000000..c5493983 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.Designer.cs @@ -0,0 +1,2762 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010082905_ClusterNodes")] + partial class ClusterNodes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.cs b/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.cs new file mode 100644 index 00000000..ca9f937b --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010082905_ClusterNodes.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ClusterNodes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClusterNodes", + columns: table => new + { + Id = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Host = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + StartedOn = table.Column(type: "datetime(6)", nullable: false), + LastSeenOn = table.Column(type: "datetime(6)", nullable: false), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + DataSources = table.Column(type: "tinyint(1)", nullable: false), + Runtimes = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ClusterNodes", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ClusterNodes_LastSeenOn", + table: "ClusterNodes", + column: "LastSeenOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClusterNodes"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.Designer.cs b/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.Designer.cs new file mode 100644 index 00000000..b09163a0 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.Designer.cs @@ -0,0 +1,2779 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010084338_ApiKeyUses")] + partial class ApiKeyUses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.cs b/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.cs new file mode 100644 index 00000000..02f62b9a --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010084338_ApiKeyUses.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ApiKeyUses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeyUses", + columns: table => new + { + PartnerId = table.Column(type: "int", nullable: false), + KeyName = table.Column(type: "varchar(500)", maxLength: 500, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + LastUsedOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeyUses", x => new { x.PartnerId, x.KeyName }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeyUses"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.Designer.cs b/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.Designer.cs new file mode 100644 index 00000000..73f63ad3 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.Designer.cs @@ -0,0 +1,2817 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010085306_OperatorActionsAndLastSignIn")] + partial class OperatorActionsAndLastSignIn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.cs b/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.cs new file mode 100644 index 00000000..d5a1d621 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261010085306_OperatorActionsAndLastSignIn.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class OperatorActionsAndLastSignIn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastSignInOn", + table: "Accounts", + type: "datetime(6)", + nullable: true); + + migrationBuilder.CreateTable( + name: "OperatorActions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Action = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Target = table.Column(type: "varchar(500)", maxLength: 500, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Detail = table.Column(type: "varchar(2000)", maxLength: 2000, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + AccountId = table.Column(type: "varchar(50)", maxLength: 50, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + OccurredOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OperatorActions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "LastSignInOn", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_OperatorActions_OccurredOn", + table: "OperatorActions", + column: "OccurredOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OperatorActions"); + + migrationBuilder.DropColumn( + name: "LastSignInOn", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261011085210_Metrics.Designer.cs b/SW.Bitween.MySql/Migrations/20261011085210_Metrics.Designer.cs new file mode 100644 index 00000000..c3b6d566 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261011085210_Metrics.Designer.cs @@ -0,0 +1,3083 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011085210_Metrics")] + partial class Metrics + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime(6)"); + + b.Property("AckRate") + .HasColumnType("double"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("double"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261011085210_Metrics.cs b/SW.Bitween.MySql/Migrations/20261011085210_Metrics.cs new file mode 100644 index 00000000..42ba6dce --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261011085210_Metrics.cs @@ -0,0 +1,212 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class Metrics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DurationMs", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "HandlerVersion", + table: "XchangeResults", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "MapperVersion", + table: "XchangeResults", + type: "varchar(50)", + unicode: false, + maxLength: 50, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "NodeName", + table: "XchangeResults", + type: "varchar(100)", + unicode: false, + maxLength: 100, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ResidentHealth", + table: "ClusterNodes", + type: "varchar(4000)", + maxLength: 4000, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AdapterStats", + columns: table => new + { + Hour = table.Column(type: "datetime(6)", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AdapterId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Version = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Role = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Runtime = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Calls = table.Column(type: "int", nullable: false), + Failures = table.Column(type: "int", nullable: false), + Timeouts = table.Column(type: "int", nullable: false), + LimitStops = table.Column(type: "int", nullable: false), + Crashes = table.Column(type: "int", nullable: false), + DurationSumMs = table.Column(type: "bigint", nullable: false), + DurationMaxMs = table.Column(type: "bigint", nullable: false), + D0 = table.Column(type: "int", nullable: false), + D1 = table.Column(type: "int", nullable: false), + D2 = table.Column(type: "int", nullable: false), + D3 = table.Column(type: "int", nullable: false), + D4 = table.Column(type: "int", nullable: false), + D5 = table.Column(type: "int", nullable: false), + D6 = table.Column(type: "int", nullable: false), + D7 = table.Column(type: "int", nullable: false), + D8 = table.Column(type: "int", nullable: false), + D9 = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AdapterStats", x => new { x.Hour, x.Node, x.AdapterId, x.Version, x.Role, x.Runtime }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ErrorStats", + columns: table => new + { + Hour = table.Column(type: "datetime(6)", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Scope = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ScopeKey = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Fingerprint = table.Column(type: "varchar(16)", unicode: false, maxLength: 16, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Sample = table.Column(type: "varchar(500)", maxLength: 500, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Count = table.Column(type: "int", nullable: false), + LastSeenOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ErrorStats", x => new { x.Hour, x.Node, x.Scope, x.ScopeKey, x.Fingerprint }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExchangeStats", + columns: table => new + { + Hour = table.Column(type: "datetime(6)", nullable: false), + Node = table.Column(type: "varchar(100)", unicode: false, maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SubscriptionId = table.Column(type: "int", nullable: false), + PartnerId = table.Column(type: "int", nullable: false), + Total = table.Column(type: "int", nullable: false), + Succeeded = table.Column(type: "int", nullable: false), + BadResponse = table.Column(type: "int", nullable: false), + Failed = table.Column(type: "int", nullable: false), + FailedAtMapper = table.Column(type: "int", nullable: false), + FailedAtHandler = table.Column(type: "int", nullable: false), + Retries = table.Column(type: "int", nullable: false), + ManualRetries = table.Column(type: "int", nullable: false), + BytesIn = table.Column(type: "bigint", nullable: false), + BytesOut = table.Column(type: "bigint", nullable: false), + DurationSumMs = table.Column(type: "bigint", nullable: false), + DurationMaxMs = table.Column(type: "bigint", nullable: false), + D0 = table.Column(type: "int", nullable: false), + D1 = table.Column(type: "int", nullable: false), + D2 = table.Column(type: "int", nullable: false), + D3 = table.Column(type: "int", nullable: false), + D4 = table.Column(type: "int", nullable: false), + D5 = table.Column(type: "int", nullable: false), + D6 = table.Column(type: "int", nullable: false), + D7 = table.Column(type: "int", nullable: false), + D8 = table.Column(type: "int", nullable: false), + D9 = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExchangeStats", x => new { x.Hour, x.Node, x.SubscriptionId, x.PartnerId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QueueSnapshots", + columns: table => new + { + At = table.Column(type: "datetime(6)", nullable: false), + QueueDepth = table.Column(type: "bigint", nullable: false), + RetryBacklog = table.Column(type: "bigint", nullable: false), + DeadLetterBacklog = table.Column(type: "bigint", nullable: false), + IncomingRate = table.Column(type: "double", nullable: false), + AckRate = table.Column(type: "double", nullable: false), + Consumers = table.Column(type: "int", nullable: false), + UnhealthyConsumers = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QueueSnapshots", x => x.At); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdapterStats"); + + migrationBuilder.DropTable( + name: "ErrorStats"); + + migrationBuilder.DropTable( + name: "ExchangeStats"); + + migrationBuilder.DropTable( + name: "QueueSnapshots"); + + migrationBuilder.DropColumn( + name: "DurationMs", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "HandlerVersion", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "MapperVersion", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "NodeName", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "ResidentHealth", + table: "ClusterNodes"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.Designer.cs b/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.Designer.cs new file mode 100644 index 00000000..1fb2a646 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.Designer.cs @@ -0,0 +1,3164 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011104343_ServiceAccounts")] + partial class ServiceAccounts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint unsigned"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresOn") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime(6)"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime(6)"); + + b.Property("AckRate") + .HasColumnType("double"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("double"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.cs b/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.cs new file mode 100644 index 00000000..2171b8be --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261011104343_ServiceAccounts.cs @@ -0,0 +1,91 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ServiceAccounts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Kind", + table: "Accounts", + type: "tinyint unsigned", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.CreateTable( + name: "ServiceTokens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AccountId = table.Column(type: "int", nullable: false), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Hash = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Shown = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ExpiresOn = table.Column(type: "datetime(6)", nullable: false), + RevokedOn = table.Column(type: "datetime(6)", nullable: true), + RevokedBy = table.Column(type: "varchar(200)", maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + LastUsedOn = table.Column(type: "datetime(6)", nullable: true), + LastUsedFrom = table.Column(type: "varchar(64)", unicode: false, maxLength: 64, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedOn = table.Column(type: "datetime(6)", nullable: true), + ModifiedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceTokens", x => x.Id); + table.ForeignKey( + name: "FK_ServiceTokens_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + column: "Kind", + value: (byte)0); + + migrationBuilder.CreateIndex( + name: "IX_ServiceTokens_AccountId", + table: "ServiceTokens", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceTokens_Hash", + table: "ServiceTokens", + column: "Hash", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ServiceTokens"); + + migrationBuilder.DropColumn( + name: "Kind", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.Designer.cs b/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.Designer.cs new file mode 100644 index 00000000..a930ad81 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.Designer.cs @@ -0,0 +1,3167 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012094154_GatewayFailureAnswer")] + partial class GatewayFailureAnswer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint unsigned"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresOn") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime(6)"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FailureAnswer") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime(6)"); + + b.Property("AckRate") + .HasColumnType("double"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("double"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.cs b/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.cs new file mode 100644 index 00000000..7bce7576 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261012094154_GatewayFailureAnswer.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class GatewayFailureAnswer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailureAnswer", + table: "ApiGateways", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailureAnswer", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.Designer.cs b/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.Designer.cs new file mode 100644 index 00000000..eb9060aa --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.Designer.cs @@ -0,0 +1,3173 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012095627_ExpectTraffic")] + partial class ExpectTraffic + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("tinyint unsigned"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresOn") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime(6)"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Changes") + .HasColumnType("longtext"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("varchar(10)"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OccurredOn"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn"); + + b.ToTable("AuditEntries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AcquiredOn") + .HasColumnType("datetime(6)"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Term") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("ClusterLeases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("varchar(8000)"); + + b.HasKey("AdapterId", "InstanceKey", "Name"); + + b.ToTable("AdapterStates", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CpuLimitSamples") + .HasColumnType("int"); + + b.Property("CpuPercentLimit") + .HasColumnType("double"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("int"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("LastHeartbeatOn") + .HasColumnType("datetime(6)"); + + b.Property("LastKnownState") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OwnedByNode") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DataSources", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WorkGroupId"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique(); + + b.ToTable("DataSourceStatements", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .IsUnicode(false) + .HasColumnType("varchar(400)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("SeenOn") + .HasColumnType("datetime(6)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("SeenOn"); + + b.ToTable("InboundMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.Property("RunFailures") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.Property("RetiredOn") + .HasColumnType("datetime(6)"); + + b.Property("ValidationSchema") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FailureAnswer") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") + .HasColumnType("int"); + + b.Property("LimitStops") + .HasColumnType("int"); + + b.Property("Timeouts") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); + + b.ToTable("AdapterStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("BadResponse") + .HasColumnType("int"); + + b.Property("BytesIn") + .HasColumnType("bigint"); + + b.Property("BytesOut") + .HasColumnType("bigint"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failed") + .HasColumnType("int"); + + b.Property("FailedAtHandler") + .HasColumnType("int"); + + b.Property("FailedAtMapper") + .HasColumnType("int"); + + b.Property("ManualRetries") + .HasColumnType("int"); + + b.Property("Retries") + .HasColumnType("int"); + + b.Property("Succeeded") + .HasColumnType("int"); + + b.Property("Total") + .HasColumnType("int"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); + + b.ToTable("ExchangeStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("datetime(6)"); + + b.Property("AckRate") + .HasColumnType("double"); + + b.Property("Consumers") + .HasColumnType("int"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); + + b.Property("IncomingRate") + .HasColumnType("double"); + + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LoginIdentity") + .IsUnique(); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ExpectTraffic") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("QuietSince") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("RunOnBadResponses") + .HasColumnType("tinyint(1)"); + + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("SourceValues") + .HasColumnType("longtext"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("XchangeId"); + + b.HasIndex("NotifierId", "FinishedOn"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("DurationMs") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.cs b/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.cs new file mode 100644 index 00000000..97851e1e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20261012095627_ExpectTraffic.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ExpectTraffic : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExpectTraffic", + table: "Subscriptions", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "QuietSince", + table: "Subscriptions", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExpectTraffic", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "QuietSince", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs index bf97e6be..6f0c6444 100644 --- a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs @@ -58,12 +58,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FailedLoginCount") .HasColumnType("int"); + b.Property("Kind") + .HasColumnType("tinyint unsigned"); + + b.Property("LastSignInOn") + .HasColumnType("datetime(6)"); + b.Property("LockoutEnd") .HasColumnType("datetime(6)"); b.Property("LoginMethods") .HasColumnType("tinyint unsigned"); + b.Property("MicrosoftIdentity") + .HasColumnType("longtext"); + b.Property("ModifiedBy") .HasColumnType("longtext"); @@ -99,6 +108,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Email = "admin@Bitween.systems", EmailProvider = (byte)0, FailedLoginCount = 0, + Kind = (byte)0, LoginMethods = (byte)2, MustChangePassword = true, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", @@ -137,6 +147,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LoginMethod") .HasColumnType("tinyint unsigned"); + b.Property("SupersededOn") + .HasColumnType("datetime(6)"); + b.HasKey("Id"); b.HasIndex("AccountId"); @@ -216,6 +229,234 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresOn") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevokedOn") + .HasColumnType("datetime(6)"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("ServiceTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId"); + + b.ToTable("AdapterDrafts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DraftId") + .HasColumnType("int"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterReleases", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdapterId", "OccurredOn"); + + b.ToTable("AdapterSourceAccesses", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("LastUsedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("PartnerId", "KeyName"); + + b.ToTable("ApiKeyUses", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => { b.Property("Id") @@ -292,6 +533,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ClusterLeases", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("DataSources") + .HasColumnType("tinyint(1)"); + + b.Property("Host") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("Runtimes") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenOn"); + + b.ToTable("ClusterNodes", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => { b.Property("AdapterId") @@ -509,6 +792,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("On") .HasColumnType("datetime(6)"); + b.Property("RunFailures") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("On"); @@ -558,6 +844,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RetiredOn") .HasColumnType("datetime(6)"); + b.Property("ValidationSchema") + .HasColumnType("longtext"); + b.HasKey("Id"); b.HasIndex("BusMessageTypeName") @@ -600,6 +889,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime(6)"); + b.Property("FailureAnswer") + .HasColumnType("int"); + b.Property("Inactive") .HasColumnType("tinyint(1)"); @@ -648,146 +940,427 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ApiGatewayId") .HasColumnType("int"); - b.Property("PartnerId") + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DataSourceId") + .HasColumnType("int"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Endpoint") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("EndpointProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DataSourceId"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SecretProperties") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Scope", "Key") + .IsUnique(); + + b.ToTable("IdempotencyKeys", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("datetime(6)"); + + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("AdapterId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Role") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Runtime") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Calls") + .HasColumnType("int"); + + b.Property("Crashes") + .HasColumnType("int"); + + b.Property("D0") + .HasColumnType("int"); + + b.Property("D1") + .HasColumnType("int"); + + b.Property("D2") + .HasColumnType("int"); + + b.Property("D3") + .HasColumnType("int"); + + b.Property("D4") + .HasColumnType("int"); + + b.Property("D5") + .HasColumnType("int"); + + b.Property("D6") + .HasColumnType("int"); + + b.Property("D7") + .HasColumnType("int"); + + b.Property("D8") + .HasColumnType("int"); + + b.Property("D9") + .HasColumnType("int"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint"); + + b.Property("DurationSumMs") + .HasColumnType("bigint"); + + b.Property("Failures") .HasColumnType("int"); - b.Property("SubscriptionId") + b.Property("LimitStops") .HasColumnType("int"); - b.Property("CreatedBy") - .HasColumnType("longtext"); + b.Property("Timeouts") + .HasColumnType("int"); - b.Property("CreatedOn") - .HasColumnType("datetime(6)"); + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime"); - b.Property("ModifiedBy") - .HasColumnType("longtext"); + b.ToTable("AdapterStats", (string)null); + }); - b.Property("ModifiedOn") + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") .HasColumnType("datetime(6)"); - b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); - b.HasIndex("PartnerId"); + b.Property("Scope") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); - b.HasIndex("SubscriptionId"); + b.Property("ScopeKey") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); - b.ToTable("ApiGatewayPartners", (string)null); - }); + b.Property("Fingerprint") + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("Count") .HasColumnType("int"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("LastSeenOn") + .HasColumnType("datetime(6)"); - b.Property("CreatedBy") - .HasColumnType("longtext"); + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); - b.Property("CreatedOn") + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint"); + + b.ToTable("ErrorStats", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") .HasColumnType("datetime(6)"); - b.Property("DataSourceId") - .HasColumnType("int"); + b.Property("Node") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); - b.Property("DocumentId") + b.Property("SubscriptionId") .HasColumnType("int"); - b.Property("Endpoint") - .HasMaxLength(500) - .IsUnicode(false) - .HasColumnType("varchar(500)"); + b.Property("PartnerId") + .HasColumnType("int"); - b.Property("EndpointProperties") - .HasColumnType("longtext"); + b.Property("BadResponse") + .HasColumnType("int"); - b.Property("Inactive") - .HasColumnType("tinyint(1)"); + b.Property("BytesIn") + .HasColumnType("bigint"); - b.Property("ModifiedBy") - .HasColumnType("longtext"); + b.Property("BytesOut") + .HasColumnType("bigint"); - b.Property("ModifiedOn") - .HasColumnType("datetime(6)"); + b.Property("D0") + .HasColumnType("int"); - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("varchar(200)"); + b.Property("D1") + .HasColumnType("int"); - b.HasKey("Id"); + b.Property("D2") + .HasColumnType("int"); - b.HasIndex("DataSourceId"); + b.Property("D3") + .HasColumnType("int"); - b.HasIndex("DocumentId"); + b.Property("D4") + .HasColumnType("int"); - b.ToTable("BusGateways", (string)null); - }); + b.Property("D5") + .HasColumnType("int"); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("D6") .HasColumnType("int"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("D7") + .HasColumnType("int"); - b.Property("BusGatewayId") + b.Property("D8") .HasColumnType("int"); - b.Property("CreatedBy") - .HasColumnType("longtext"); + b.Property("D9") + .HasColumnType("int"); - b.Property("CreatedOn") - .HasColumnType("datetime(6)"); + b.Property("DurationMaxMs") + .HasColumnType("bigint"); - b.Property("MatchExpression") - .HasColumnType("longtext"); + b.Property("DurationSumMs") + .HasColumnType("bigint"); - b.Property("ModifiedBy") - .HasColumnType("longtext"); + b.Property("Failed") + .HasColumnType("int"); - b.Property("ModifiedOn") - .HasColumnType("datetime(6)"); + b.Property("FailedAtHandler") + .HasColumnType("int"); - b.Property("PartnerId") + b.Property("FailedAtMapper") .HasColumnType("int"); - b.Property("SubscriptionId") + b.Property("ManualRetries") .HasColumnType("int"); - b.HasKey("Id"); + b.Property("Retries") + .HasColumnType("int"); - b.HasIndex("BusGatewayId"); + b.Property("Succeeded") + .HasColumnType("int"); - b.HasIndex("PartnerId"); + b.Property("Total") + .HasColumnType("int"); - b.HasIndex("SubscriptionId"); + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId"); - b.ToTable("BusGatewayRoutes", (string)null); + b.ToTable("ExchangeStats", (string)null); }); - modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => { - b.Property("Id") - .HasMaxLength(200) - .IsUnicode(false) - .HasColumnType("varchar(200)"); + b.Property("At") + .HasColumnType("datetime(6)"); - b.Property("Name") - .HasColumnType("longtext"); + b.Property("AckRate") + .HasColumnType("double"); - b.Property("SecretProperties") - .HasColumnType("longtext"); + b.Property("Consumers") + .HasColumnType("int"); - b.Property("Values") - .HasColumnType("longtext"); + b.Property("DeadLetterBacklog") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.Property("IncomingRate") + .HasColumnType("double"); - b.ToTable("GlobalAdapterValuesSets", (string)null); + b.Property("QueueDepth") + .HasColumnType("bigint"); + + b.Property("RetryBacklog") + .HasColumnType("bigint"); + + b.Property("UnhealthyConsumers") + .HasColumnType("int"); + + b.HasKey("At"); + + b.ToTable("QueueSnapshots", (string)null); }); modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => @@ -876,6 +1449,84 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OnHoldXchanges", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("OccurredOn") + .HasColumnType("datetime(6)"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("OccurredOn"); + + b.ToTable("OperatorActions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ClaimedUntil") + .HasColumnType("datetime(6)"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Headers") + .HasColumnType("longtext"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PublishedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn"); + + b.ToTable("OutboxMessages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => { b.Property("Id") @@ -1080,6 +1731,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregationTarget") .HasColumnType("tinyint unsigned"); + b.Property("AutoPauseAfterFailures") + .HasColumnType("int"); + b.Property("CategoryId") .HasColumnType("int"); @@ -1098,6 +1752,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("ExpectTraffic") + .HasColumnType("longtext"); + b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -1106,6 +1763,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HandlerProperties") .HasColumnType("longtext"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("Inactive") .HasColumnType("tinyint(1)"); @@ -1123,6 +1785,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MapperProperties") .HasColumnType("longtext"); + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("MatchExpression") .HasColumnType("longtext"); @@ -1134,9 +1801,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PartnerId") .HasColumnType("int"); + b.Property("PausedAutomatically") + .HasColumnType("tinyint(1)"); + b.Property("PausedOn") .HasColumnType("datetime(6)"); + b.Property("QuietSince") + .HasColumnType("datetime(6)"); + b.Property("ReceiveOn") .HasColumnType("datetime(6)"); @@ -1148,6 +1821,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ReceiverProperties") .HasColumnType("longtext"); + b.Property("ReceiverVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("ResponseMessageTypeName") .HasMaxLength(500) .IsUnicode(false) @@ -1162,6 +1840,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RunOnBadResponses") .HasColumnType("tinyint(1)"); + b.Property("RunningSince") + .HasColumnType("datetime(6)"); + b.Property("Temporary") .HasColumnType("tinyint(1)"); @@ -1176,6 +1857,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ValidatorProperties") .HasColumnType("longtext"); + b.Property("ValidatorVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + b.Property("WorkGroupId") .HasColumnType("int"); @@ -1425,6 +2111,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Success") .HasColumnType("tinyint(1)"); + b.Property("Suppressed") + .HasColumnType("tinyint(1)"); + b.Property("XchangeId") .HasMaxLength(50) .IsUnicode(false) @@ -1434,6 +2123,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("XchangeId"); + b.HasIndex("NotifierId", "FinishedOn"); + b.ToTable("XchangeNotifications", (string)null); }); @@ -1470,12 +2161,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AttemptNumber") .HasColumnType("int"); + b.Property("DurationMs") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("longtext"); b.Property("FinishedOn") .HasColumnType("datetime(6)"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("NodeName") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + b.Property("OutputBad") .HasColumnType("tinyint(1)"); @@ -2089,6 +2798,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => { b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") @@ -2202,6 +2920,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(500)"); + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + b1.Property("Name") .IsRequired() .HasMaxLength(500) diff --git a/SW.Bitween.NativeAdapters/AzureBlobReceiver/NativeAzureBlobReceiver.cs b/SW.Bitween.NativeAdapters/AzureBlobReceiver/NativeAzureBlobReceiver.cs index 503a7d54..c4e25722 100644 --- a/SW.Bitween.NativeAdapters/AzureBlobReceiver/NativeAzureBlobReceiver.cs +++ b/SW.Bitween.NativeAdapters/AzureBlobReceiver/NativeAzureBlobReceiver.cs @@ -62,7 +62,7 @@ public async Task DeleteFile(string fileId) var sourceBlob = _container.GetBlobClient(fileId); // Retry-safe: if a prior attempt already moved/deleted this file, there's - // nothing left to do — treat it as already completed, not an error. + // nothing left to do - treat it as already completed, not an error. if (!await sourceBlob.ExistsAsync()) return; diff --git a/SW.Bitween.NativeAdapters/AzureBlobReceiver/adapter.json b/SW.Bitween.NativeAdapters/AzureBlobReceiver/adapter.json new file mode 100644 index 00000000..cd34d201 --- /dev/null +++ b/SW.Bitween.NativeAdapters/AzureBlobReceiver/adapter.json @@ -0,0 +1,64 @@ +{ + "manifestVersion": 1, + "id": "nativeazureblobreceiver", + "displayName": "Azure Blob pickup", + "summary": "Picks up blobs from an Azure Storage container on a schedule.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "azure", + "blob", + "storage", + "file" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "ConnectionString", + "type": "text", + "required": true, + "secret": true, + "description": "Azure Storage account connection string." + }, + { + "name": "ContainerName", + "type": "text", + "required": true, + "default": "", + "description": "Name of the blob container to read files from." + }, + { + "name": "FolderName", + "type": "text", + "description": "Folder (blob name prefix) within the container to read files from." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of files to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode file contents (e.g. utf8)." + }, + { + "name": "DeleteMovesFileTo", + "type": "text", + "description": "Folder to move a file to after it\u0027s received. If empty, the file is deleted instead." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/AzureBlobUploadHandler/adapter.json b/SW.Bitween.NativeAdapters/AzureBlobUploadHandler/adapter.json new file mode 100644 index 00000000..b33abc4f --- /dev/null +++ b/SW.Bitween.NativeAdapters/AzureBlobUploadHandler/adapter.json @@ -0,0 +1,52 @@ +{ + "manifestVersion": 1, + "id": "nativeazureblobuploadhandler", + "displayName": "Azure Blob upload", + "summary": "Writes the message as a blob to an Azure Storage container.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "azure", + "blob", + "storage", + "file" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "ConnectionString", + "type": "text", + "required": true, + "secret": true, + "description": "Azure Storage account connection string." + }, + { + "name": "ContainerName", + "type": "text", + "required": true, + "default": "", + "description": "Name of the blob container to upload files to." + }, + { + "name": "FileName", + "type": "text", + "description": "Name to give the uploaded blob. If empty, a name is generated from the current timestamp and a GUID." + }, + { + "name": "FileExtension", + "type": "text", + "description": "Extension appended to the generated blob name (e.g. json, csv). Ignored if FileName is set." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/DestinationCircuits.cs b/SW.Bitween.NativeAdapters/DestinationCircuits.cs new file mode 100644 index 00000000..3e75bcf1 --- /dev/null +++ b/SW.Bitween.NativeAdapters/DestinationCircuits.cs @@ -0,0 +1,262 @@ +using System.Collections.Concurrent; +using System.Diagnostics.Metrics; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Renci.SshNet.Common; + +namespace SW.Bitween.NativeAdapters; + +/// +/// When a destination counts as unwell, and for how long calls to it then fail at once. +/// +/// Unwell results in a row that open the circuit. 0 switches the breaker off. +/// The run of failures must fall within this many seconds. +/// How long an open circuit fails calls before it lets one through to try. +public sealed record CircuitBreakerSettings(int Failures = 5, int WindowSeconds = 60, int OpenSeconds = 60) +{ + public static CircuitBreakerSettings Default { get; } = new(); + public static CircuitBreakerSettings Off { get; } = new(0); +} + +/// A call refused because its destination's circuit is open. It never reached the destination. +public sealed class CircuitOpenException(string destination, string message) : Exception(message) +{ + public string Destination { get; } = destination; +} + +/// +/// A circuit breaker per destination, shared by the built-in outbound adapters of this process. +/// +/// +/// +/// A delivery to a partner that has stopped answering holds its place in the processing queue for +/// its whole timeout. Enough of them filled every place, and flows to healthy partners waited +/// behind them. Once a destination has failed times in +/// a row, calls to it fail at once, as a 503 so retry policies that retry a 503 retry them, and the +/// queue moves on. After the cool-down one call is let through to try: success closes the circuit, +/// failure opens it again. +/// +/// +/// Only failures that say the destination itself is unwell count: timeouts, refused or dropped +/// connections, 429, and 5xx other than 501. A 4xx is the partner rejecting one message, and is no +/// reason to stop sending it others. Each replica learns on its own; nothing is shared. +/// +/// +public sealed class DestinationCircuits +{ + /// The same meter name as Bitween's own, so the exporter that takes those takes these. + static readonly Meter Meter = new("Bitween"); + + static readonly Counter Opened = Meter.CreateCounter("bitween.circuits.opened", + description: "Destination circuits opened after failing too many calls in a row"); + + /// No breaker: every call goes through. What an adapter built without one uses. + public static DestinationCircuits None { get; } = new(CircuitBreakerSettings.Off); + + readonly CircuitBreakerSettings settings; + readonly TimeProvider time; + readonly ILogger logger; + readonly ConcurrentDictionary circuits = new(StringComparer.OrdinalIgnoreCase); + + public DestinationCircuits(CircuitBreakerSettings settings, TimeProvider? time = null, + ILogger? logger = null) + { + this.settings = settings; + this.time = time ?? TimeProvider.System; + this.logger = logger ?? (ILogger)NullLogger.Instance; + } + + public bool Enabled => settings.Failures > 0; + + TimeSpan Window => TimeSpan.FromSeconds(Math.Max(1, settings.WindowSeconds)); + TimeSpan Cooldown => TimeSpan.FromSeconds(Math.Max(1, settings.OpenSeconds)); + + /// + /// Admits a call to , or throws + /// when its circuit is open. The pass is told how the call went; disposed untold, it changes nothing. + /// + public CircuitPass Enter(string destination) + { + // Only destinations that have failed have a circuit, so a healthy one costs a lookup. + if (!Enabled || !circuits.TryGetValue(destination, out var circuit)) + return new CircuitPass(this, destination, null, trial: false); + + lock (circuit) + { + var now = time.GetUtcNow(); + switch (circuit.State) + { + case CircuitState.Closed: + return new CircuitPass(this, destination, circuit, trial: false); + + case CircuitState.Open when now >= circuit.OpenUntil: + case CircuitState.HalfOpen when now >= circuit.TrialStarted + Cooldown: + // A trial that never reported back (its call left hanging) doesn't keep the circuit shut. + circuit.State = CircuitState.HalfOpen; + circuit.TrialStarted = now; + return new CircuitPass(this, destination, circuit, trial: true); + + default: + throw new CircuitOpenException(destination, + $"503: ServiceUnavailable: circuit open for {destination} ({circuit.Reason}; " + + (circuit.State == CircuitState.HalfOpen + ? "a trial call is under way)" + : $"trying again after {circuit.OpenUntil.UtcDateTime.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}Z)")); + } + } + } + + internal void Report(string destination, Circuit? circuit, bool trial, Outcome outcome) + { + if (!Enabled) return; + if (circuit is null) + { + if (outcome != Outcome.Unwell) return; + circuit = circuits.GetOrAdd(destination, _ => new Circuit()); + } + + string? opened = null; + var closed = false; + lock (circuit) + { + var now = time.GetUtcNow(); + switch (outcome) + { + case Outcome.Well: + // The destination answered: whatever state it was in, it is reachable again. + closed = circuit.State != CircuitState.Closed; + circuit.State = CircuitState.Closed; + circuit.Failures.Clear(); + break; + + case Outcome.Unwell when circuit.State == CircuitState.Closed: + circuit.Failures.Enqueue(now); + while (circuit.Failures.Count > 0 && now - circuit.Failures.Peek() > Window) + circuit.Failures.Dequeue(); + if (circuit.Failures.Count >= settings.Failures) + opened = Open(circuit, now, $"{circuit.Failures.Count} failures in a row"); + break; + + case Outcome.Unwell when circuit.State == CircuitState.HalfOpen && trial: + opened = Open(circuit, now, "the trial call failed"); + break; + + case Outcome.Neutral when circuit.State == CircuitState.HalfOpen && trial: + // Said nothing about the destination (a bad template, a refused login): let the next call try. + circuit.TrialStarted = DateTimeOffset.MinValue; + break; + + // A late failure from a call admitted before the circuit opened says nothing new. + } + } + + if (opened is not null) + { + Opened.Add(1, new KeyValuePair("destination", destination)); + logger.LogWarning( + "Circuit for {Destination} opened ({Reason}): calls to it fail at once until {Until:u}, then one is let through to try.", + destination, opened, circuit.OpenUntil); + } + else if (closed) + logger.LogInformation("Circuit for {Destination} closed: it answered again.", destination); + } + + string Open(Circuit circuit, DateTimeOffset now, string reason) + { + circuit.State = CircuitState.Open; + circuit.OpenUntil = now + Cooldown; + circuit.Reason = reason; + circuit.Failures.Clear(); + return reason; + } + + /// Whether an HTTP status says the destination is unwell rather than that it rejected this one message. + /// + /// 500 counts: one message that breaks the partner gets a 500, but it takes a run of them with no + /// success between to open the circuit, and that run is a partner in trouble. 501 doesn't: it + /// means this endpoint will never do what was asked, which a pause won't change. + /// + public static bool IsUnwell(HttpStatusCode status) => + status == HttpStatusCode.TooManyRequests || + ((int)status >= 500 && status != HttpStatusCode.NotImplemented); + + /// Whether a failure says the destination couldn't be reached or didn't answer in time. + public static bool IsUnwell(Exception exception) + { + for (var e = exception; e is not null; e = e.InnerException) + { + switch (e) + { + case CircuitOpenException: + return false; + case TimeoutException or OperationCanceledException or SocketException: + case IOException and not (FileNotFoundException or DirectoryNotFoundException): + case SshConnectionException or SshOperationTimeoutException or ProxyException: + case SmtpProtocolException: + case SmtpCommandException { StatusCode: SmtpStatusCode.ServiceNotAvailable }: + return true; + case HttpRequestException { StatusCode: { } status }: + return IsUnwell(status); + case HttpRequestException { HttpRequestError: HttpRequestError.ConnectionError or HttpRequestError.ResponseEnded }: + return true; + } + } + return false; + } + + internal enum Outcome { Neutral, Well, Unwell } + + internal enum CircuitState { Closed, Open, HalfOpen } + + internal sealed class Circuit + { + public CircuitState State = CircuitState.Closed; + public readonly Queue Failures = new(); + public DateTimeOffset OpenUntil; + public DateTimeOffset TrialStarted; + public string Reason = ""; + } +} + +/// One admitted call. Told or ; disposed untold, it is neutral. +public sealed class CircuitPass : IDisposable +{ + readonly DestinationCircuits circuits; + readonly string destination; + readonly DestinationCircuits.Circuit? circuit; + readonly bool trial; + bool reported; + + internal CircuitPass(DestinationCircuits circuits, string destination, DestinationCircuits.Circuit? circuit, bool trial) + { + this.circuits = circuits; + this.destination = destination; + this.circuit = circuit; + this.trial = trial; + } + + /// The destination answered, whatever it said. + public void Succeeded() => Report(DestinationCircuits.Outcome.Well); + + /// The destination is unwell: it timed out, refused, dropped the connection, or answered 429 or a 5xx. + public void Failed() => Report(DestinationCircuits.Outcome.Unwell); + + /// Reports how a call that threw went: unwell when the exception says so, otherwise nothing. + public void Failed(Exception exception) + { + if (DestinationCircuits.IsUnwell(exception)) Failed(); + } + + public void Dispose() => Report(DestinationCircuits.Outcome.Neutral); + + void Report(DestinationCircuits.Outcome outcome) + { + if (reported) return; + reported = true; + circuits.Report(destination, circuit, trial, outcome); + } +} diff --git a/SW.Bitween.NativeAdapters/FileTransfer.cs b/SW.Bitween.NativeAdapters/FileTransfer.cs new file mode 100644 index 00000000..1a202cc7 --- /dev/null +++ b/SW.Bitween.NativeAdapters/FileTransfer.cs @@ -0,0 +1,245 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using FluentFTP; +using Renci.SshNet; +using Renci.SshNet.Common; + +namespace SW.Bitween.NativeAdapters; + +/// A file on the server, as a receiver lists it. +internal record RemoteFile(string Name, DateTime? LastWriteTimeUtc); + +/// +/// One connection to a file server, signed in: the few things the FTP and SFTP adapters do with +/// one. Paths that aren't absolute are relative to the directory changed into. +/// +internal interface IFileTransfer : IAsyncDisposable +{ + Task ChangeDirectoryAsync(string path); + Task> ListFilesAsync(); + Task DownloadAsync(string path, Stream into); + Task UploadAsync(Stream from, string path); + Task ExistsAsync(string path); + Task DeleteAsync(string path); + Task RenameAsync(string from, string to); +} + +/// SFTP, over SSH.NET. +internal sealed class SftpTransfer(SftpClient client) : IFileTransfer +{ + /// + /// Signs in with the private key when one is given - its passphrase, if any, is the password - + /// and with the password otherwise. A pinned host key is checked during the key exchange, + /// before authentication: a server presenting any other key is turned away before a password + /// or key signature is sent to it. Every step waits at most : SSH.NET + /// waits for ever by default, and a server that signs in but never opens the SFTP session held + /// the delivery, and with it a place in the processing queue, until the server went away. + /// + public static async Task ConnectAsync(string host, int? port, string username, + string? password, string? privateKey, string? hostKeyFingerprint, TimeSpan timeout) + { + AuthenticationMethod authentication; + if (!string.IsNullOrWhiteSpace(privateKey)) + { + using var keyStream = new MemoryStream(Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(privateKey))); + var key = string.IsNullOrEmpty(password) + ? new PrivateKeyFile(keyStream) + : new PrivateKeyFile(keyStream, password); + authentication = new PrivateKeyAuthenticationMethod(username, key); + } + else if (!string.IsNullOrEmpty(password)) + authentication = new PasswordAuthenticationMethod(username, password); + else + throw new ArgumentException("SFTP needs a password or a private key to sign in with."); + + var client = new SftpClient(new ConnectionInfo(host, port ?? 22, username, authentication) { Timeout = timeout }) + { + OperationTimeout = timeout, + }; + string? refusedKey = null; + client.HostKeyReceived += (_, e) => + { + if (string.IsNullOrWhiteSpace(hostKeyFingerprint)) return; + e.CanTrust = FtpProtocol.NormalizeFingerprint(e.FingerPrintSHA256) == + FtpProtocol.NormalizeFingerprint(hostKeyFingerprint); + if (!e.CanTrust) refusedKey = FtpProtocol.NormalizeFingerprint(e.FingerPrintSHA256); + }; + + try + { + // ConnectAsync covers the connection and sign-in; OperationTimeout, the SFTP session it then opens. + using var connecting = new CancellationTokenSource(timeout * 2); + await client.ConnectAsync(connecting.Token); + } + catch (SshConnectionException) when (refusedKey is not null) + { + client.Dispose(); + throw new InvalidOperationException( + $"The SFTP server's host key (SHA256:{refusedKey}) is not the one configured. " + + "Refusing to send credentials to it."); + } + catch + { + client.Dispose(); + throw; + } + + return new SftpTransfer(client); + } + + public Task ChangeDirectoryAsync(string path) => client.ChangeDirectoryAsync(path); + + public async Task> ListFilesAsync() + { + var files = new List(); + await foreach (var file in client.ListDirectoryAsync(".", CancellationToken.None)) + if (file.IsRegularFile) + files.Add(new RemoteFile(file.Name, file.LastWriteTimeUtc)); + return files; + } + + public Task DownloadAsync(string path, Stream into) => client.DownloadFileAsync(path, into); + + public Task UploadAsync(Stream from, string path) => client.UploadFileAsync(from, path); + + public Task ExistsAsync(string path) => client.ExistsAsync(path); + + public Task DeleteAsync(string path) => client.DeleteFileAsync(path, CancellationToken.None); + + public Task RenameAsync(string from, string to) => client.RenameFileAsync(from, to, CancellationToken.None); + + public ValueTask DisposeAsync() + { + try + { + if (client.IsConnected) client.Disconnect(); + } + finally + { + client.Dispose(); + } + + return ValueTask.CompletedTask; + } +} + +/// FTP and FTPS, over FluentFTP. +internal sealed class FtpTransfer(AsyncFtpClient client) : IFileTransfer +{ + /// + /// Connects in the given encryption mode - none, explicit (AUTH TLS, port 21) or + /// implicit (TLS from the first byte, port 990). With TLS, the server's certificate must + /// be valid, or, when a thumbprint is pinned, be exactly that certificate - which is how a + /// partner's self-signed certificate is trusted. Either way the check is made during the TLS + /// handshake, before the credentials are sent. + /// + public static async Task ConnectAsync(string host, int? port, string username, string password, + string encryption, string? certificateThumbprint, bool passive) + { + if (string.IsNullOrEmpty(password)) + throw new ArgumentException("FTP needs a password to sign in with."); + + var mode = (encryption ?? "").Trim().ToLowerInvariant() switch + { + "" or "none" => FtpEncryptionMode.None, + "explicit" => FtpEncryptionMode.Explicit, + "implicit" => FtpEncryptionMode.Implicit, + _ => throw new ArgumentException($"Unknown Encryption '{encryption}': use none, explicit or implicit."), + }; + + var client = new AsyncFtpClient(host, username, password, + port ?? (mode == FtpEncryptionMode.Implicit ? 990 : 21), + new FtpConfig + { + EncryptionMode = mode, + DataConnectionType = passive ? FtpDataConnectionType.AutoPassive : FtpDataConnectionType.AutoActive, + // Listing times in UTC, so a file's age is measured as over SFTP. + TimeConversion = FtpDate.UTC, + }); + + string? refusedCertificate = null; + client.ValidateCertificate += (_, e) => + { + if (string.IsNullOrWhiteSpace(certificateThumbprint)) + { + e.Accept = e.PolicyErrors == System.Net.Security.SslPolicyErrors.None; + return; + } + + var presented = Thumbprint(e.Certificate); + e.Accept = presented == NormalizeThumbprint(certificateThumbprint); + if (!e.Accept) refusedCertificate = presented; + }; + + try + { + await client.Connect(); + } + catch (Exception) when (refusedCertificate is not null) + { + await client.DisposeAsync(); + throw new InvalidOperationException( + $"The FTPS server's certificate (SHA-256 {refusedCertificate}) is not the one configured. " + + "Refusing to send credentials to it."); + } + catch + { + await client.DisposeAsync(); + throw; + } + + return new FtpTransfer(client); + } + + /// The certificate's SHA-256 thumbprint, as upper-case hex. + internal static string Thumbprint(X509Certificate certificate) => + Convert.ToHexString(SHA256.HashData(certificate.GetRawCertData())); + + /// "AB:CD ef", "abcdef" and "SHA256:ABCDEF" are the same thumbprint. + internal static string NormalizeThumbprint(string thumbprint) + { + var value = thumbprint.Trim(); + if (value.StartsWith("SHA256:", StringComparison.OrdinalIgnoreCase)) + value = value[7..]; + return new string(value.Where(Uri.IsHexDigit).ToArray()).ToUpperInvariant(); + } + + public Task ChangeDirectoryAsync(string path) => client.SetWorkingDirectory(path); + + public async Task> ListFilesAsync() => + (await client.GetListing()) + .Where(i => i.Type == FtpObjectType.File) + .Select(i => new RemoteFile(i.Name, i.Modified == DateTime.MinValue ? null : i.Modified)) + .ToList(); + + public async Task DownloadAsync(string path, Stream into) + { + if (!await client.DownloadStream(into, path)) + throw new IOException($"'{path}' could not be downloaded."); + } + + public async Task UploadAsync(Stream from, string path) + { + if (await client.UploadStream(from, path, FtpRemoteExists.Overwrite) == FtpStatus.Failed) + throw new IOException($"'{path}' could not be uploaded."); + } + + public Task ExistsAsync(string path) => client.FileExists(path); + + public Task DeleteAsync(string path) => client.DeleteFile(path); + + public Task RenameAsync(string from, string to) => client.Rename(from, to); + + public async ValueTask DisposeAsync() + { + try + { + if (client.IsConnected) await client.Disconnect(); + } + finally + { + await client.DisposeAsync(); + } + } +} diff --git a/SW.Bitween.NativeAdapters/FileTransfers/Adapters.cs b/SW.Bitween.NativeAdapters/FileTransfers/Adapters.cs new file mode 100644 index 00000000..63cf4ff5 --- /dev/null +++ b/SW.Bitween.NativeAdapters/FileTransfers/Adapters.cs @@ -0,0 +1,216 @@ +using System.Text; +using SW.Bitween.NativeAdapters.JsonMapper; +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters.FileTransfers; + +/// +/// Picking files up from a file server, the same whichever protocol reaches it: the SFTP and FTP +/// pickups differ only in how they connect. Behaves as the Rebex FTP receiver does. +/// +public abstract class FilePickup : INativeInfolinkReceiver, IDisposable + where TSettings : IPickupSettings, new() +{ + protected TSettings Options { get; private set; } = new(); + private IFileTransfer? _transfer; + + private IFileTransfer Transfer => _transfer + ?? throw new SWException($"The {Name} was used before Initialize() ran."); + + private protected abstract Task ConnectAsync(); + + public async Task Initialize() + { + _transfer = await ConnectAsync(); + if (!string.IsNullOrEmpty(Options.TargetPath)) + await _transfer.ChangeDirectoryAsync(Options.TargetPath); + } + + public async Task Finalize() + { + if (_transfer is not null) await _transfer.DisposeAsync(); + _transfer = null; + } + + /// Closes a connection a run left open - one that failed before Finalize. + public void Dispose() + { + try { _transfer?.DisposeAsync().AsTask().GetAwaiter().GetResult(); } catch { /* closing only */ } + _transfer = null; + } + + public async Task> ListFiles() + { + var files = await Transfer.ListFilesAsync(); + + // A file still being uploaded is listed like any other; taking it half-written sends half a + // document on, then deletes the rest as it arrives. Left alone until it has been still for a while. + var settledBefore = DateTime.UtcNow.AddSeconds(-Options.MinimumFileAgeSeconds); + + return files + .Where(f => Options.MinimumFileAgeSeconds <= 0 || f.LastWriteTimeUtc is not { } written || written <= settledBefore) + .Take(Options.BatchSize) + .Select(f => f.Name) + .ToList(); + } + + public async Task GetFile(string fileId) + { + await using var stream = new MemoryStream(); + await Transfer.DownloadAsync(fileId, stream); + var data = stream.ToArray(); + + return Options.ResponseEncoding.ToLower() switch + { + "base64" => new XchangeFile(Convert.ToBase64String(data), fileId), + "utf8" => new XchangeFile(Encoding.UTF8.GetString(data), fileId), + _ => throw new ArgumentException($"Unknown ResponseEncoding '{Options.ResponseEncoding}'") + }; + } + + public async Task DeleteFile(string fileId) + { + if (Options.CheckFileExistence && !await Transfer.ExistsAsync(fileId)) + return; + + if (string.IsNullOrWhiteSpace(Options.DeleteMovesFileTo)) + await Transfer.DeleteAsync(fileId); + else + await Transfer.RenameAsync(fileId, Options.DeleteMovesFileTo + "/" + fileId); + } + + public abstract string Name { get; } + + public void InitializeStartupValues(IDictionary settings) => + Options = settings.ConvertTo(); + + public Type StartupValuesType => typeof(TSettings); +} + +/// +/// Uploading the message as a file, the same whichever protocol reaches the server. Behaves as the +/// Rebex FTP upload handler does. +/// +public abstract class FileUpload(DestinationCircuits? circuits) : INativeInfolinkHandler + where TSettings : IUploadSettings, new() +{ + readonly DestinationCircuits circuits = circuits ?? DestinationCircuits.None; + + protected TSettings Options { get; private set; } = new(); + + private protected abstract Task ConnectAsync(); + + /// The server this uploads to, as its circuit is known: protocol, host and port. + private protected abstract string Destination { get; } + + public async Task Handle(XchangeFile xchangeFile) + { + // Read before connecting, so a payload that can't be decoded never opens a connection. + var bytes = Options.DataEncoding.ToLower() switch + { + "base64" => Convert.FromBase64String(xchangeFile.Data), + "utf8" => Encoding.UTF8.GetBytes(xchangeFile.Data), + _ => throw new ArgumentException($"Unknown DataEncoding '{Options.DataEncoding}'") + }; + + var filename = NameFor(xchangeFile, Options, DateTime.UtcNow); + + using var pass = circuits.Enter(Destination); + try + { + await using var transfer = await ConnectAsync(); + await using var stream = new MemoryStream(bytes); + // The path the Rebex adapter writes to, an empty TargetPath included. + await transfer.UploadAsync(stream, $"{Options.TargetPath}/{filename}"); + } + catch (Exception e) + { + pass.Failed(e); + throw; + } + pass.Succeeded(); + + return new XchangeFile(string.Empty); + } + + /// + /// What the uploaded file is called: the FileName setting when there is one, filled from a JSON + /// message's fields (order-{{ orderId }}.json), else the message's own name, else the time; then + /// the prefix. Without a name of its own, every message through one gateway carries the same name + /// ({urlName}.json), and each upload replaced the one before it. + /// + internal static string NameFor(XchangeFile file, IUploadSettings options, DateTime now) + { + string? name = null; + if (!string.IsNullOrWhiteSpace(options.FileName)) + { + // Base64 data isn't a message with fields; its template is used as written. + var data = options.DataEncoding.Equals("utf8", StringComparison.OrdinalIgnoreCase) ? file.Data : null; + name = FtpProtocol.SafeFileName(ScribanJsonHelper.FillIfJson(options.FileName, data).Trim()); + } + if (string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(file.Filename)) + name = FtpProtocol.SafeFileName(file.Filename); + if (string.IsNullOrWhiteSpace(name)) + name = $"{now.Year:0000}{now.Month:00}{now.Day:00}{now.Hour:00}{now.Minute:00}{now.Second:00}{now.Millisecond:000}"; + + return string.IsNullOrWhiteSpace(options.FileNamePrefix) ? name : $"{options.FileNamePrefix}_{name}"; + } + + public abstract string Name { get; } + + public void InitializeStartupValues(IDictionary settings) => + Options = settings.ConvertTo(); + + public Type StartupValuesType => typeof(TSettings); +} + +internal static class SftpTimeouts +{ + /// The setting, or 60 seconds when it is missing or not above 0: never without a limit. + public static TimeSpan Of(int seconds) => TimeSpan.FromSeconds(seconds > 0 ? seconds : 60); +} + +/// Picks up files from an SFTP server, over SSH.NET. No license needed. +public class NativeSftpReceiver : FilePickup +{ + private protected override Task ConnectAsync() => + SftpTransfer.ConnectAsync(Options.Host, Options.Port, Options.Username, Options.Password, + Options.PrivateKey, Options.HostKeyFingerprint, SftpTimeouts.Of(Options.TimeoutSeconds)); + + public override string Name => "NativeSftpReceiver"; +} + +/// Uploads the message as a file to an SFTP server, over SSH.NET. No license needed. +public class NativeSftpUploadHandler(DestinationCircuits? circuits = null) : FileUpload(circuits) +{ + private protected override string Destination => $"sftp://{Options.Host}:{Options.Port ?? 22}"; + + private protected override Task ConnectAsync() => + SftpTransfer.ConnectAsync(Options.Host, Options.Port, Options.Username, Options.Password, + Options.PrivateKey, Options.HostKeyFingerprint, SftpTimeouts.Of(Options.TimeoutSeconds)); + + public override string Name => "NativeSftpUploadHandler"; +} + +/// Picks up files from an FTP or FTPS server, over FluentFTP. No license needed. +public class NativeFtpReceiver : FilePickup +{ + private protected override Task ConnectAsync() => + FtpTransfer.ConnectAsync(Options.Host, Options.Port, Options.Username, Options.Password!, + Options.Encryption, Options.CertificateThumbprint, Options.PassiveMode); + + public override string Name => "NativeFtpReceiver"; +} + +/// Uploads the message as a file to an FTP or FTPS server, over FluentFTP. No license needed. +public class NativeFtpUploadHandler(DestinationCircuits? circuits = null) : FileUpload(circuits) +{ + private protected override string Destination => + $"ftp://{Options.Host}:{Options.Port ?? ((Options.Encryption ?? "").Trim().Equals("implicit", StringComparison.OrdinalIgnoreCase) ? 990 : 21)}"; + + private protected override Task ConnectAsync() => + FtpTransfer.ConnectAsync(Options.Host, Options.Port, Options.Username, Options.Password!, + Options.Encryption, Options.CertificateThumbprint, Options.PassiveMode); + + public override string Name => "NativeFtpUploadHandler"; +} diff --git a/SW.Bitween.NativeAdapters/FileTransfers/Settings.cs b/SW.Bitween.NativeAdapters/FileTransfers/Settings.cs new file mode 100644 index 00000000..e72b5246 --- /dev/null +++ b/SW.Bitween.NativeAdapters/FileTransfers/Settings.cs @@ -0,0 +1,165 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace SW.Bitween.NativeAdapters.FileTransfers; + +/// What every pickup from a file server is told, whichever protocol reaches it. +public interface IPickupSettings +{ + string? TargetPath { get; } + int BatchSize { get; } + string ResponseEncoding { get; } + string? DeleteMovesFileTo { get; } + bool CheckFileExistence { get; } + int MinimumFileAgeSeconds { get; } +} + +/// What every upload to a file server is told, whichever protocol reaches it. +public interface IUploadSettings +{ + string? TargetPath { get; } + string? FileNamePrefix { get; } + string? FileName { get; } + string DataEncoding { get; } +} + +/// Reaching an SFTP server: the names and meanings the Rebex FTP adapters use for SFTP. +public abstract class SftpConnectionSettings +{ + [Required] + [Description("SFTP server hostname.")] + public string Host { get; set; } = string.Empty; + + [Description("Server port. If empty, 22 is used.")] + public int? Port { get; set; } + + [Required] + [Description("SFTP account username.")] + public string Username { get; set; } = string.Empty; + + [Secure] + [Description("Account password, or the private key's passphrase when a private key is set.")] + public string? Password { get; set; } + + [Secure] + [Description("Private key to sign in with instead of a password, in PEM or OpenSSH format. A PEM key pasted onto one line is re-wrapped.")] + public string? PrivateKey { get; set; } + + [Description("SHA-256 fingerprint of the server's host key, as 'ssh-keygen -lf' prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent.")] + public string? HostKeyFingerprint { get; set; } + + [DefaultValue(60)] + [Description("Seconds to wait for the server at each step - connecting, opening the SFTP session, each request - before the run fails. Default 60.")] + public int TimeoutSeconds { get; set; } = 60; +} + +/// Reaching an FTP or FTPS server. +public abstract class FtpConnectionSettings +{ + [Required] + [Description("FTP server hostname.")] + public string Host { get; set; } = string.Empty; + + [Description("Server port. If empty, 990 is used for implicit FTPS and 21 otherwise.")] + public int? Port { get; set; } + + [Required] + [Description("FTP account username.")] + public string Username { get; set; } = string.Empty; + + [Required] + [Secure] + [Description("FTP account password.")] + public string? Password { get; set; } + + [DefaultValue("none")] + [Description("none for plain FTP, explicit for FTPS that upgrades the connection with AUTH TLS (usually port 21), or implicit for FTPS that is TLS from the start (usually port 990).")] + public string Encryption { get; set; } = "none"; + + [Description("For FTPS: the SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before credentials are sent. When empty, the certificate must be valid.")] + public string? CertificateThumbprint { get; set; } + + [DefaultValue(true)] + [Description("Passive mode, where the client opens the data connections; what almost every server behind a firewall needs. Turn off for active mode.")] + public bool PassiveMode { get; set; } = true; +} + +public class SftpPickupSettings : SftpConnectionSettings, IPickupSettings +{ + [Description("Remote directory to read files from.")] + public string? TargetPath { get; set; } + + [DefaultValue(50)] + [Description("Maximum number of files to fetch per polling batch.")] + public int BatchSize { get; set; } = 50; + + [DefaultValue("utf8")] + [Description("Encoding used to decode file contents: utf8, or base64 for binary files.")] + public string ResponseEncoding { get; set; } = "utf8"; + + [Description("Folder to move a file to after it's received. If empty, the file is deleted instead.")] + public string? DeleteMovesFileTo { get; set; } + + [DefaultValue(true)] + [Description("Whether to verify the file still exists before attempting to delete or move it after receiving.")] + public bool CheckFileExistence { get; set; } = true; + + [Description("Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed.")] + public int MinimumFileAgeSeconds { get; set; } +} + +public class FtpPickupSettings : FtpConnectionSettings, IPickupSettings +{ + [Description("Remote directory to read files from.")] + public string? TargetPath { get; set; } + + [DefaultValue(50)] + [Description("Maximum number of files to fetch per polling batch.")] + public int BatchSize { get; set; } = 50; + + [DefaultValue("utf8")] + [Description("Encoding used to decode file contents: utf8, or base64 for binary files.")] + public string ResponseEncoding { get; set; } = "utf8"; + + [Description("Folder to move a file to after it's received. If empty, the file is deleted instead.")] + public string? DeleteMovesFileTo { get; set; } + + [DefaultValue(true)] + [Description("Whether to verify the file still exists before attempting to delete or move it after receiving.")] + public bool CheckFileExistence { get; set; } = true; + + [Description("Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed.")] + public int MinimumFileAgeSeconds { get; set; } +} + +public class SftpUploadSettings : SftpConnectionSettings, IUploadSettings +{ + [Description("Remote directory to upload the file into.")] + public string? TargetPath { get; set; } + + [Description("Prefix added to the uploaded file's name.")] + public string? FileNamePrefix { get; set; } + + [Description("The uploaded file's name, in place of the message's own. For a JSON message it may use the message's fields, as order-{{ orderId }}.json, so each message gets its own file. Empty keeps the message's own name, or the time when it has none.")] + public string? FileName { get; set; } + + [DefaultValue("utf8")] + [Description("Encoding used to write the file contents: utf8, or base64 to write the decoded bytes.")] + public string DataEncoding { get; set; } = "utf8"; +} + +public class FtpUploadSettings : FtpConnectionSettings, IUploadSettings +{ + [Description("Remote directory to upload the file into.")] + public string? TargetPath { get; set; } + + [Description("Prefix added to the uploaded file's name.")] + public string? FileNamePrefix { get; set; } + + [Description("The uploaded file's name, in place of the message's own. For a JSON message it may use the message's fields, as order-{{ orderId }}.json, so each message gets its own file. Empty keeps the message's own name, or the time when it has none.")] + public string? FileName { get; set; } + + [DefaultValue("utf8")] + [Description("Encoding used to write the file contents: utf8, or base64 to write the decoded bytes.")] + public string DataEncoding { get; set; } = "utf8"; +} diff --git a/SW.Bitween.NativeAdapters/FtpProtocol.cs b/SW.Bitween.NativeAdapters/FtpProtocol.cs index 68fb7437..6d3c4c3f 100644 --- a/SW.Bitween.NativeAdapters/FtpProtocol.cs +++ b/SW.Bitween.NativeAdapters/FtpProtocol.cs @@ -25,4 +25,47 @@ public static void EnsurePrivateKeyProvided(string protocol, string? privateKey) if (protocol.ToLower() is "sftpssh" && string.IsNullOrWhiteSpace(privateKey)) throw new ArgumentException($"A private key is required for the '{protocol}' protocol."); } + + /// + /// Refuses an SFTP server whose host key is not the one configured, before any credential is + /// sent to it. Without a pinned key the client accepts whatever key the server presents, so + /// anyone who can intercept the connection receives the password and the files. Left + /// unconfigured, the connection is accepted as before; set it to the SHA-256 fingerprint that + /// ssh-keygen -lf prints for the server's key. + /// + public static void VerifyHostKey(Rebex.Net.Sftp sftp, string? expectedFingerprint) + { + if (string.IsNullOrWhiteSpace(expectedFingerprint)) + return; + + var actual = sftp.ServerKey.Fingerprint.ToString(Rebex.Security.Certificates.SignatureHashAlgorithm.SHA256, true); + if (NormalizeFingerprint(actual) != NormalizeFingerprint(expectedFingerprint)) + { + sftp.Disconnect(); + throw new InvalidOperationException( + $"The SFTP server's host key (SHA256:{NormalizeFingerprint(actual)}) is not the one configured. " + + "Refusing to send credentials to it."); + } + } + + /// "SHA256:abc=" and "abc" are the same fingerprint, as OpenSSH and Rebex print it. + internal static string NormalizeFingerprint(string fingerprint) + { + var value = fingerprint.Trim(); + if (value.StartsWith("SHA256:", StringComparison.OrdinalIgnoreCase)) + value = value[7..]; + return value.TrimEnd('='); + } + + /// + /// One file name, with no directory in it. The name comes from the exchange - a POP3 receiver + /// sets it from the email's subject - so "../../etc/x" would otherwise climb out of the + /// configured target directory on the partner's server. + /// + internal static string SafeFileName(string filename) + { + var name = filename.Replace('\\', '/'); + name = name[(name.LastIndexOf('/') + 1)..].Trim(); + return name is "" or "." or ".." ? null : name; + } } diff --git a/SW.Bitween.NativeAdapters/FtpReceiver/adapter.json b/SW.Bitween.NativeAdapters/FtpReceiver/adapter.json new file mode 100644 index 00000000..e56b013a --- /dev/null +++ b/SW.Bitween.NativeAdapters/FtpReceiver/adapter.json @@ -0,0 +1,104 @@ +{ + "manifestVersion": 1, + "id": "nativeftpreceiver", + "displayName": "FTP / FTPS pickup", + "summary": "Picks up files from an FTP server, plain or over TLS (explicit or implicit FTPS), on a schedule, leaving files still being written for the next run.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "ftp", + "ftps", + "file" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "FTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, 990 is used for implicit FTPS and 21 otherwise." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "FTP account username." + }, + { + "name": "Password", + "type": "text", + "required": true, + "secret": true, + "description": "FTP account password." + }, + { + "name": "Encryption", + "type": "text", + "default": "none", + "description": "none for plain FTP, explicit for FTPS that upgrades the connection with AUTH TLS (usually port 21), or implicit for FTPS that is TLS from the start (usually port 990)." + }, + { + "name": "CertificateThumbprint", + "type": "text", + "description": "For FTPS: the SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before credentials are sent. When empty, the certificate must be valid." + }, + { + "name": "PassiveMode", + "type": "boolean", + "default": "True", + "description": "Passive mode, where the client opens the data connections; what almost every server behind a firewall needs. Turn off for active mode." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to read files from." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of files to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode file contents: utf8, or base64 for binary files." + }, + { + "name": "DeleteMovesFileTo", + "type": "text", + "description": "Folder to move a file to after it's received. If empty, the file is deleted instead." + }, + { + "name": "CheckFileExistence", + "type": "boolean", + "default": "True", + "description": "Whether to verify the file still exists before attempting to delete or move it after receiving." + }, + { + "name": "MinimumFileAgeSeconds", + "type": "number", + "default": "0", + "description": "Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/FtpUploadHandler/adapter.json b/SW.Bitween.NativeAdapters/FtpUploadHandler/adapter.json new file mode 100644 index 00000000..f043721e --- /dev/null +++ b/SW.Bitween.NativeAdapters/FtpUploadHandler/adapter.json @@ -0,0 +1,91 @@ +{ + "manifestVersion": 1, + "id": "nativeftpuploadhandler", + "displayName": "FTP / FTPS upload", + "summary": "Uploads the message as a file to an FTP server, plain or over TLS (explicit or implicit FTPS).", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "ftp", + "ftps", + "file" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "FTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, 990 is used for implicit FTPS and 21 otherwise." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "FTP account username." + }, + { + "name": "Password", + "type": "text", + "required": true, + "secret": true, + "description": "FTP account password." + }, + { + "name": "Encryption", + "type": "text", + "default": "none", + "description": "none for plain FTP, explicit for FTPS that upgrades the connection with AUTH TLS (usually port 21), or implicit for FTPS that is TLS from the start (usually port 990)." + }, + { + "name": "CertificateThumbprint", + "type": "text", + "description": "For FTPS: the SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before credentials are sent. When empty, the certificate must be valid." + }, + { + "name": "PassiveMode", + "type": "boolean", + "default": "True", + "description": "Passive mode, where the client opens the data connections; what almost every server behind a firewall needs. Turn off for active mode." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to upload the file into." + }, + { + "name": "FileNamePrefix", + "type": "text", + "description": "Prefix added to the uploaded file's name." + }, + { + "name": "FileName", + "type": "text", + "description": "The uploaded file's name, in place of the message's own. For a JSON message it may use the message's fields, as order-{{ orderId }}.json, so each message gets its own file. Empty keeps the message's own name, or the time when it has none." + }, + { + "name": "DataEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to write the file contents: utf8, or base64 to write the decoded bytes." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/HttpHandler/DynamicHttpProxy.cs b/SW.Bitween.NativeAdapters/HttpHandler/DynamicHttpProxy.cs index c6574707..3a1733a0 100644 --- a/SW.Bitween.NativeAdapters/HttpHandler/DynamicHttpProxy.cs +++ b/SW.Bitween.NativeAdapters/HttpHandler/DynamicHttpProxy.cs @@ -10,6 +10,9 @@ public interface IDynamicHttpProxy } public class DynamicHttpProxy(IHttpClientFactory httpClientFactory) : BackgroundService, IDynamicHttpProxy { + /// The named client every adapter call goes through; it carries the address guard. + public const string ClientName = "bitween-adapters"; + private readonly ConcurrentDictionary _cache = new(); private readonly Channel _usageChannel = Channel.CreateUnbounded(); @@ -24,8 +27,11 @@ public HttpClient GetClient(string fullUrl) // Fast path: No lock, thread-safe read var client = _cache.GetOrAdd(origin, key => { - var newClient = httpClientFactory.CreateClient(key); + var newClient = httpClientFactory.CreateClient(ClientName); newClient.BaseAddress = new Uri(key); + // Shared by every subscription calling this origin, so a timeout here would be one size + // for all of them; each request sets its own instead (HttpHandlerInput.TimeoutSeconds). + newClient.Timeout = Timeout.InfiniteTimeSpan; return newClient; }); diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HeaderList.cs b/SW.Bitween.NativeAdapters/HttpHandler/HeaderList.cs new file mode 100644 index 00000000..a7e7cd2a --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/HeaderList.cs @@ -0,0 +1,34 @@ +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters; + +/// +/// The HTTP adapters' Headers setting: Name:Value entries separated by commas, as it has +/// always been read. +/// +/// +/// The setting's description promised name=value lines, and the way people naturally type +/// a list - a space after each comma - made a header name with a leading space, which .NET refuses: +/// every delivery failed with a FormatException. Both are read now, alongside the original form: +/// names and values are trimmed, newlines separate as commas do, empty entries are skipped, and +/// = separates an entry that has no colon. An entry that is neither says which it was. +/// +internal static class HeaderList +{ + public static IEnumerable> Parse(string? headers) + { + if (string.IsNullOrWhiteSpace(headers)) yield break; + + foreach (var entry in headers.Split([',', '\n', '\r'], + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + // The first colon only: a value may hold one of its own (a URL, a time). + var separator = entry.IndexOf(':'); + if (separator < 0) separator = entry.IndexOf('='); + if (separator <= 0) + throw new SWException($"The header '{entry}' needs a name and a value, written Name:Value."); + + yield return new KeyValuePair(entry[..separator].Trim(), entry[(separator + 1)..].Trim()); + } + } +} diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs index 91d7de22..cb39e9ec 100644 --- a/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs +++ b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs @@ -40,6 +40,8 @@ public class HttpHandlerInput public string? Headers { get; set; } [Description("Field name or JSON path used to extract a correlation ID from the response.")] public string? CorrelationId { get; set; } + [Description("Seconds to wait for the endpoint to answer before the delivery fails. Default 100.")] + public int TimeoutSeconds { get; set; } = 100; [Description("OAuth2 client ID.")] public string? ClientId { get; set; } [Secure] diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HttpLogin.cs b/SW.Bitween.NativeAdapters/HttpHandler/HttpLogin.cs index f3731a75..2120624b 100644 --- a/SW.Bitween.NativeAdapters/HttpHandler/HttpLogin.cs +++ b/SW.Bitween.NativeAdapters/HttpHandler/HttpLogin.cs @@ -20,8 +20,9 @@ public static async Task GetToken(HttpClient client, string loginUrl, st ? JsonConvert.SerializeObject(defaultBody) : RenderBody(loginBody, username, password); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); var response = await client.PostAsync(new Uri(loginUrl), - new StringContent(body, Encoding.UTF8, "application/json")); + new StringContent(body, Encoding.UTF8, "application/json"), timeout.Token); var responseBody = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) throw new SWException( diff --git a/SW.Bitween.NativeAdapters/HttpHandler/NativeHttpHandler.cs b/SW.Bitween.NativeAdapters/HttpHandler/NativeHttpHandler.cs index d7e5975c..e6f1c26b 100644 --- a/SW.Bitween.NativeAdapters/HttpHandler/NativeHttpHandler.cs +++ b/SW.Bitween.NativeAdapters/HttpHandler/NativeHttpHandler.cs @@ -7,8 +7,10 @@ namespace SW.Bitween.NativeAdapters; -public class NativeHttpHandler(IDynamicHttpProxy httpProxy) : INativeInfolinkHandler +public class NativeHttpHandler(IDynamicHttpProxy httpProxy, DestinationCircuits? circuits = null) : INativeInfolinkHandler { + readonly DestinationCircuits circuits = circuits ?? DestinationCircuits.None; + private HttpMethod HttpMethodFromString(string method) { switch (method.ToLower()) @@ -19,6 +21,8 @@ private HttpMethod HttpMethodFromString(string method) return HttpMethod.Delete; case "put": return HttpMethod.Put; + case "patch": + return HttpMethod.Patch; default: return HttpMethod.Post; } @@ -31,17 +35,40 @@ private HttpMethod HttpMethodFromString(string method) public async Task Handle(XchangeFile xchangeFile) { HttpClient client = httpProxy.GetClient(_options.Url); + + Uri uri; + if (!string.IsNullOrEmpty(xchangeFile.Data) && _options.Url.Contains("{{")) + { + Template parsedTemplate = Template.Parse(_options.Url); + IDictionary obj = + JsonConvert.DeserializeObject>(xchangeFile.Data, + new DictionaryConverter()) ?? new Dictionary(); + Hash jsonHash = Hash.FromDictionary(obj); + uri = new Uri(parsedTemplate.Render(jsonHash)); + EnsureSameOrigin(_options.Url, uri); + } + else + uri = new Uri(_options.Url); + + // Before signing in, so a destination whose circuit is open costs no login call either. + // Userinfo is left out of the key: it is a credential, and the key is logged. + using var pass = circuits.Enter($"{uri.Scheme}://{uri.Authority}"); + + // Credentials go on this request, never on the client: the proxy shares one client per + // origin across every subscription that calls it, so a default header set here was sent on + // the next subscription's call too - with its credentials, or with none of its own. + string? apiKey = null; + AuthenticationHeaderValue? authorization = null; if (_options.AuthType == "ApiKey") - client.DefaultRequestHeaders.Add("ApiKey", _options.ApiKey); + apiKey = _options.ApiKey; else if (_options.AuthType == "Bearer") - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", _options.LoginPassword); + authorization = new AuthenticationHeaderValue("Bearer", _options.LoginPassword); else if (_options.AuthType == "Basic") { string credentials = Convert.ToBase64String( Encoding.ASCII.GetBytes(_options.LoginUsername + ":" + _options.LoginPassword)); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); + authorization = new AuthenticationHeaderValue("Basic", credentials); } else if (_options.AuthType == "Login") { @@ -52,22 +79,12 @@ public async Task Handle(XchangeFile xchangeFile) Email = _options.LoginUsername, Password = _options.LoginPassword }); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + authorization = new AuthenticationHeaderValue("Bearer", token); } else if (_options.AuthType == "OAuth2") { - var oathRequest = new HttpRequestMessage(HttpMethod.Post, _options.LoginUrl); - var oauthContentDictionary = new List>(); - oauthContentDictionary.Add(new("client_id", _options.ClientId!)); - oauthContentDictionary.Add(new("client_secret", _options.ClientSecret!)); - oauthContentDictionary.Add(new("grant_type", "client_credentials")); - var oauthContent = new FormUrlEncodedContent(oauthContentDictionary); - oathRequest.Content = oauthContent; - var oauthResponse = await client.SendAsync(oathRequest); - var res = await oauthResponse.Content.ReadAsStringAsync(); - var resDeserialized = JsonConvert.DeserializeObject(res); - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", resDeserialized?.access_token); + authorization = new AuthenticationHeaderValue("Bearer", + await OAuthTokens.GetAsync(client, _options.LoginUrl, _options.ClientId, _options.ClientSecret)); } string requestBody = xchangeFile.Data; @@ -98,19 +115,6 @@ public async Task Handle(XchangeFile xchangeFile) break; } - Uri uri; - if (!string.IsNullOrEmpty(xchangeFile.Data) && _options.Url.Contains("{{")) - { - Template parsedTemplate = Template.Parse(_options.Url); - IDictionary obj = - JsonConvert.DeserializeObject>(xchangeFile.Data, - new DictionaryConverter()) ?? new Dictionary(); - Hash jsonHash = Hash.FromDictionary(obj); - uri = new Uri(parsedTemplate.Render(jsonHash)); - } - else - uri = new Uri(_options.Url); - var httpMethod = HttpMethodFromString(_options.Verb); HttpRequestMessage request = new HttpRequestMessage() { @@ -118,29 +122,53 @@ public async Task Handle(XchangeFile xchangeFile) Method = httpMethod, Content = httpMethod == HttpMethod.Get ? null : content }; - string? headers1 = _options.Headers; - IEnumerable>? headers = headers1 != null - ? (headers1.Split(',')).Select((Func>)(h => - { - string[] strArray = h.Split(':'); - return new KeyValuePair(strArray[0], strArray[1]); - })) - : null; - if (headers != null) - { - foreach (KeyValuePair keyValuePair1 in headers) - { - KeyValuePair keyValuePair = keyValuePair1; - request.Headers.Add(keyValuePair.Key, keyValuePair.Value); - } - } + foreach (var (name, value) in HeaderList.Parse(_options.Headers)) + request.Headers.Add(name, value); + + if (authorization is not null) + request.Headers.Authorization = authorization; + if (apiKey is not null) + request.Headers.Add("ApiKey", apiKey); if (!string.IsNullOrEmpty(_options.CorrelationId)) request.Headers.Add("request-context-correlation-id", _options.CorrelationId); - HttpResponseMessage response = await client.SendAsync(request); - string resp = await response.Content.ReadAsStringAsync(); - if (response.StatusCode < HttpStatusCode.OK || response.StatusCode >= HttpStatusCode.InternalServerError) - throw new Exception($"{response.StatusCode}: {resp}"); + // The client is shared, so the timeout is this request's own rather than the client's. + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, _options.TimeoutSeconds))); + HttpResponseMessage response; + string resp; + try + { + response = await client.SendAsync(request, timeout.Token); + resp = await response.Content.ReadAsStringAsync(); + } + catch (OperationCanceledException) when (timeout.IsCancellationRequested) + { + pass.Failed(); + throw new TimeoutException($"{uri.GetLeftPart(UriPartial.Authority)} did not answer within {_options.TimeoutSeconds} seconds."); + } + catch (Exception e) + { + pass.Failed(e); + throw; + } + + if (DestinationCircuits.IsUnwell(response.StatusCode)) + pass.Failed(); + else + pass.Succeeded(); + + // 408 and 429 mean "not now", not "this message is wrong": they fail the delivery, so a retry + // policy can try again, instead of being recorded as a bad response nobody retries. + if (response.StatusCode < HttpStatusCode.OK || response.StatusCode >= HttpStatusCode.InternalServerError || + response.StatusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests) + { + var retryAfter = response.Headers.RetryAfter is { } after + ? $" Retry-After: {(after.Delta is { } delta ? $"{delta.TotalSeconds:0}s" : after.Date?.ToString("u"))}." + : ""; + // The number first, as retry policies match it ("503:"), then the name it always carried + // ("ServiceUnavailable:"), so matchers written against either keep working. + throw new Exception($"{(int)response.StatusCode}: {response.StatusCode}: {resp}{retryAfter}"); + } XchangeFile xchangeFile1 = response.StatusCode < HttpStatusCode.BadRequest ? new XchangeFile(resp) : new XchangeFile(resp, badData: true); @@ -148,6 +176,89 @@ public async Task Handle(XchangeFile xchangeFile) } + /// + /// Refuses a rendered URL that went somewhere the template does not allow. The values come + /// from the message, so without this a payload such as "path": ".attacker.example/x" + /// could send the request - and its credentials - to a host of its choosing. + /// + /// + /// The scheme must be the template's. A placeholder inside the host may fill in one DNS label + /// and no more - no dots, no slashes - so https://{{tenant}}.api.example still reaches + /// any tenant of api.example and nothing else. The port must be the template's unless the + /// template leaves it to a placeholder, and the message may not add a user name. + /// + internal static void EnsureSameOrigin(string template, Uri rendered) + { + var schemeEnd = template.IndexOf("://", StringComparison.Ordinal); + if (schemeEnd < 0 || template[..schemeEnd].Contains("{{")) return; + + var authority = AuthorityOf(template, schemeEnd + 3); + if (!authority.Contains("{{")) + { + // Nothing in the message can reach the host when the template spells it out. + return; + } + + var at = LastIndexOutsidePlaceholders(authority, '@'); + var hostAndPort = at < 0 ? authority : authority[(at + 1)..]; + + string hostTemplate = hostAndPort, portTemplate = null; + var colon = LastIndexOutsidePlaceholders(hostAndPort, ':'); + if (colon >= 0 && !hostAndPort.StartsWith('[')) + { + hostTemplate = hostAndPort[..colon]; + portTemplate = hostAndPort[(colon + 1)..]; + } + + var pattern = "^" + string.Join("[a-z0-9-]*", + System.Text.RegularExpressions.Regex.Split(hostTemplate.ToLowerInvariant(), @"\{\{.*?\}\}") + .Select(System.Text.RegularExpressions.Regex.Escape)) + "$"; + + var allowed = + string.Equals(template[..schemeEnd], rendered.Scheme, StringComparison.OrdinalIgnoreCase) && + System.Text.RegularExpressions.Regex.IsMatch(rendered.IdnHost.ToLowerInvariant(), pattern) && + (portTemplate is null + ? rendered.IsDefaultPort + : portTemplate.Contains("{{") || portTemplate == rendered.Port.ToString()) && + (at >= 0 || string.IsNullOrEmpty(rendered.UserInfo)); + + if (!allowed) + throw new SWException( + $"The message's values changed where this request goes: the URL template allows {authority} " + + $"but rendered to {rendered.GetLeftPart(UriPartial.Authority)}."); + } + + /// The authority of a URL template, read past placeholders so a '/' inside one doesn't end it. + private static string AuthorityOf(string template, int start) + { + var i = start; + while (i < template.Length) + { + if (template.AsSpan(i).StartsWith("{{")) + { + var close = template.IndexOf("}}", i + 2, StringComparison.Ordinal); + i = close < 0 ? template.Length : close + 2; + continue; + } + if (template[i] is '/' or '?' or '#') break; + i++; + } + return template[start..i]; + } + + private static int LastIndexOutsidePlaceholders(string value, char c) + { + var found = -1; + var depth = false; + for (var i = 0; i < value.Length; i++) + { + if (!depth && value.AsSpan(i).StartsWith("{{")) { depth = true; i++; continue; } + if (depth && value.AsSpan(i).StartsWith("}}")) { depth = false; i++; continue; } + if (!depth && value[i] == c) found = i; + } + return found; + } + public string Name => "NativeHttpHandler"; public void InitializeStartupValues(IDictionary settings) { diff --git a/SW.Bitween.NativeAdapters/HttpHandler/OAuthTokens.cs b/SW.Bitween.NativeAdapters/HttpHandler/OAuthTokens.cs new file mode 100644 index 00000000..eecc2c6d --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/OAuthTokens.cs @@ -0,0 +1,64 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json.Linq; +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters; + +/// +/// OAuth2 client-credentials tokens for the HTTP adapters, fetched once and reused until shortly +/// before they expire. +/// +/// +/// A token used to be fetched for every message, which turns a burst of deliveries into a burst of +/// token requests the provider may throttle. And a failed fetch was not checked: the request went +/// out with "Bearer null", and the partner's 401 was all anyone saw. +/// +internal static class OAuthTokens +{ + private static readonly ConcurrentDictionary Cache = new(); + private static readonly TimeSpan RenewBefore = TimeSpan.FromSeconds(60); + private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(30); + + public static async Task GetAsync(HttpClient client, string? tokenUrl, string? clientId, string? clientSecret) + { + if (string.IsNullOrWhiteSpace(tokenUrl) || string.IsNullOrWhiteSpace(clientId)) + throw new SWException("OAuth2 needs LoginUrl (the token endpoint) and ClientId."); + + var key = tokenUrl + "|" + clientId + "|" + + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(clientSecret ?? ""))); + if (Cache.TryGetValue(key, out var cached) && cached.Expires - RenewBefore > DateTimeOffset.UtcNow) + return cached.Token; + + using var timeout = new CancellationTokenSource(RequestTimeout); + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, tokenUrl) + { + Content = new FormUrlEncodedContent(new Dictionary + { + ["client_id"] = clientId, + ["client_secret"] = clientSecret ?? "", + ["grant_type"] = "client_credentials", + }) + }, timeout.Token); + var body = await response.Content.ReadAsStringAsync(timeout.Token); + + if (!response.IsSuccessStatusCode) + throw new SWException($"The OAuth2 token endpoint answered {(int)response.StatusCode} {response.StatusCode}."); + + JObject json; + try { json = JObject.Parse(body); } + catch { throw new SWException("The OAuth2 token endpoint did not answer with JSON."); } + + var token = json["access_token"]?.ToString(); + if (string.IsNullOrEmpty(token)) + throw new SWException("The OAuth2 token endpoint did not return an access_token."); + + var lifetime = int.TryParse(json["expires_in"]?.ToString(), out var seconds) && seconds > 0 ? seconds : 300; + Cache[key] = (token, DateTimeOffset.UtcNow.AddSeconds(lifetime)); + return token; + } + + /// For tests: forget every cached token. + internal static void Clear() => Cache.Clear(); +} diff --git a/SW.Bitween.NativeAdapters/HttpHandler/OutboundAddressGuard.cs b/SW.Bitween.NativeAdapters/HttpHandler/OutboundAddressGuard.cs new file mode 100644 index 00000000..f848ea80 --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/OutboundAddressGuard.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Sockets; + +namespace SW.Bitween.NativeAdapters; + +/// +/// Decides which addresses the HTTP adapters may connect to, checked after DNS resolution so a +/// hostname that points somewhere forbidden is caught as well as a literal address. +/// +/// +/// +/// Link-local addresses are always refused. That range holds the cloud metadata endpoints +/// (169.254.169.254, and fd00:ec2::254 on AWS IPv6) which hand out the node's own credentials, +/// and no partner integration lives there. +/// +/// +/// Private and loopback ranges are refused only when blockPrivate is set. Bitween is +/// self-hosted and calling systems inside the customer's own network is a normal integration, so +/// refusing them by default would break deployments that do exactly that. +/// +/// +internal static class OutboundAddressGuard +{ + private static readonly IPAddress AwsMetadataV6 = IPAddress.Parse("fd00:ec2::254"); + + public static Func> ConnectCallback( + bool blockPrivate) => async (context, cancellationToken) => + { + var host = context.DnsEndPoint.Host; + var addresses = IPAddress.TryParse(host, out var literal) + ? [literal] + : await Dns.GetHostAddressesAsync(host, cancellationToken); + + var allowed = addresses.Where(a => !IsBlocked(a, blockPrivate)).ToArray(); + if (allowed.Length == 0) + throw new HttpRequestException( + $"Bitween will not connect to {host}: it resolves only to addresses adapters may not reach " + + "(link-local or cloud metadata" + (blockPrivate ? ", private or loopback" : "") + ")."); + + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + await socket.ConnectAsync(allowed, context.DnsEndPoint.Port, cancellationToken); + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + }; + + internal static bool IsBlocked(IPAddress address, bool blockPrivate) + { + if (address.IsIPv4MappedToIPv6) + address = address.MapToIPv4(); + + if (address.Equals(AwsMetadataV6) || address.IsIPv6LinkLocal) + return true; + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + var b = address.GetAddressBytes(); + if (b[0] == 169 && b[1] == 254) return true; + if (!blockPrivate) return false; + + return b[0] == 0 || + b[0] == 10 || + b[0] == 127 || + (b[0] == 100 && b[1] >= 64 && b[1] <= 127) || + (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || + (b[0] == 192 && b[1] == 168); + } + + if (!blockPrivate) return false; + + var v6 = address.GetAddressBytes(); + return IPAddress.IsLoopback(address) || + address.Equals(IPAddress.IPv6Any) || + (v6[0] & 0xFE) == 0xFC; // fc00::/7, unique local + } +} diff --git a/SW.Bitween.NativeAdapters/HttpHandler/adapter.json b/SW.Bitween.NativeAdapters/HttpHandler/adapter.json new file mode 100644 index 00000000..9a49ab92 --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/adapter.json @@ -0,0 +1,117 @@ +{ + "manifestVersion": 1, + "id": "nativehttphandler", + "displayName": "HTTP request", + "summary": "Sends the message to an HTTP endpoint, with authentication, headers and retries on 408/429.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "http", + "rest", + "api", + "webhook" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "AuthType", + "type": "text", + "description": "Authentication type (e.g. ApiKey, Basic, OAuth2, Bearer)." + }, + { + "name": "ApiKey", + "type": "text", + "secret": true, + "description": "API key used for ApiKey or Bearer authentication." + }, + { + "name": "LoginUrl", + "type": "text", + "description": "URL used to obtain a login token for OAuth2 or Basic auth flows." + }, + { + "name": "Verb", + "type": "text", + "default": "post", + "description": "HTTP verb to use when calling the endpoint (e.g. get, post, put, patch, delete)." + }, + { + "name": "LoginUsername", + "type": "text", + "description": "Username for Basic or OAuth2 password-grant authentication." + }, + { + "name": "LoginPassword", + "type": "text", + "secret": true, + "description": "Password for Basic or OAuth2 password-grant authentication." + }, + { + "name": "LoginBody", + "type": "text", + "description": "Optional custom login request (Login auth type). Put the real username and password in LoginUsername and LoginPassword, then write {{username}} and {{password}} here where they belong, e.g. {\u0022email\u0022:\u0022{{username}}\u0022,\u0022password\u0022:\u0022{{password}}\u0022}. This field is not hidden, so never type a secret into it directly. Leave empty to send the default body." + }, + { + "name": "LoginTokenPath", + "type": "text", + "description": "Optional path to the token in the login response (e.g. token, data.access_token). Empty reads the \u0027jwt\u0027 field." + }, + { + "name": "Url", + "type": "text", + "required": true, + "default": "", + "description": "The target HTTP endpoint URL." + }, + { + "name": "ContentType", + "type": "text", + "default": "application/json", + "description": "Content-Type header sent with the request (e.g. application/json, application/xml)." + }, + { + "name": "Headers", + "type": "text", + "secret": true, + "description": "Additional HTTP headers as key=value pairs separated by newlines." + }, + { + "name": "CorrelationId", + "type": "text", + "description": "Field name or JSON path used to extract a correlation ID from the response." + }, + { + "name": "TimeoutSeconds", + "type": "number", + "default": "100", + "description": "Seconds to wait for the endpoint to answer before the delivery fails. Default 100." + }, + { + "name": "ClientId", + "type": "text", + "description": "OAuth2 client ID." + }, + { + "name": "ClientSecret", + "type": "text", + "secret": true, + "description": "OAuth2 client secret." + }, + { + "name": "DefaultRequest", + "type": "text", + "description": "Default request body template used when the incoming document is empty." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/HttpReceiver/HttpReceiverInput.cs b/SW.Bitween.NativeAdapters/HttpReceiver/HttpReceiverInput.cs index 03224d2b..f51a7b1b 100644 --- a/SW.Bitween.NativeAdapters/HttpReceiver/HttpReceiverInput.cs +++ b/SW.Bitween.NativeAdapters/HttpReceiver/HttpReceiverInput.cs @@ -48,4 +48,7 @@ public class HttpReceiverInput [Description("JSON path to the array element in the response to iterate over (e.g. $.items).")] public string? ArrayPath { get; set; } -} \ No newline at end of file + + [Description("Seconds to wait for the endpoint to answer before the run fails. Default 100.")] + public int TimeoutSeconds { get; set; } = 100; +} diff --git a/SW.Bitween.NativeAdapters/HttpReceiver/NativeHttpReceiver.cs b/SW.Bitween.NativeAdapters/HttpReceiver/NativeHttpReceiver.cs index 5bce0d59..298563ed 100644 --- a/SW.Bitween.NativeAdapters/HttpReceiver/NativeHttpReceiver.cs +++ b/SW.Bitween.NativeAdapters/HttpReceiver/NativeHttpReceiver.cs @@ -32,6 +32,8 @@ private HttpMethod HttpMethodFromString(string method) return HttpMethod.Delete; case "put": return HttpMethod.Put; + case "patch": + return HttpMethod.Patch; default: return HttpMethod.Post; } @@ -50,15 +52,20 @@ public async Task Finalize() public async Task> ListFiles() { HttpClient client = httpProxy.GetClient(_options.Url); + + // On the request, never on the client - see NativeHttpHandler: the client is shared by + // every subscription calling the same origin. + string? apiKey = null; + AuthenticationHeaderValue? authorization = null; if (_options.AuthType == "ApiKey") - client.DefaultRequestHeaders.Add("ApiKey", _options.ApiKey); + apiKey = _options.ApiKey; else if (_options.AuthType == "Basic") { string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(_options.LoginUsername + ":" + _options.LoginPassword)); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); + authorization = new AuthenticationHeaderValue("Basic", credentials); } else if (_options.AuthType == "Bearer") - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _options.LoginPassword); + authorization = new AuthenticationHeaderValue("Bearer", _options.LoginPassword); else if (_options.AuthType == "Login") { string token = await HttpLogin.GetToken(client, Require(_options.LoginUrl, "LoginUrl"), _options.LoginBody, @@ -68,23 +75,12 @@ public async Task> ListFiles() UserName = _options.LoginUsername, Password = _options.LoginPassword }); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + authorization = new AuthenticationHeaderValue("Bearer", token); } else if (_options.AuthType == "OAuth2") { - var oathRequest = new HttpRequestMessage(HttpMethod.Post, _options.LoginUrl); - var oauthContentDictionary = new List>(); - oauthContentDictionary.Add(new KeyValuePair("client_id", Require(_options.ClientId, "ClientId"))); - oauthContentDictionary.Add(new KeyValuePair("client_secret", Require(_options.ClientSecret, "ClientSecret"))); - oauthContentDictionary.Add(new KeyValuePair("grant_type", "client_credentials")); - var oauthContent = new FormUrlEncodedContent(oauthContentDictionary); - oathRequest.Content = oauthContent; - var oauthResponse = await client.SendAsync(oathRequest); - var res = await oauthResponse.Content.ReadAsStringAsync(); - var resDeserialized = JsonConvert.DeserializeObject(res); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", - resDeserialized?.access_token ?? throw new SWException( - "The OAuth2 token endpoint did not return a JSON body carrying an 'access_token'.")); + authorization = new AuthenticationHeaderValue("Bearer", + await OAuthTokens.GetAsync(client, _options.LoginUrl, _options.ClientId, _options.ClientSecret)); } HttpContent? content = null; @@ -115,21 +111,23 @@ public async Task> ListFiles() Method = HttpMethodFromString(Require(_options.Verb, "Verb")), Content = content }; - string? headers1 = _options.Headers; - IEnumerable>? headers = headers1?.Split(',').Select((Func>) (h => + foreach (var (name, value) in HeaderList.Parse(_options.Headers)) + request.Headers.Add(name, value); + if (authorization is not null) + request.Headers.Authorization = authorization; + if (apiKey is not null) + request.Headers.Add("ApiKey", apiKey); + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, _options.TimeoutSeconds))); + HttpResponseMessage response; + try { - string[] strArray = h.Split(':'); - return new KeyValuePair(strArray[0], strArray[1]); - })); - if (headers != null) + response = await client.SendAsync(request, timeout.Token); + } + catch (OperationCanceledException) when (timeout.IsCancellationRequested) { - foreach (KeyValuePair keyValuePair1 in headers) - { - KeyValuePair keyValuePair = keyValuePair1; - request.Headers.Add(keyValuePair.Key, keyValuePair.Value); - } + throw new TimeoutException($"{uri.GetLeftPart(UriPartial.Authority)} did not answer within {_options.TimeoutSeconds} seconds."); } - HttpResponseMessage response = await client.SendAsync(request); if (response.StatusCode < HttpStatusCode.OK || response.StatusCode >= HttpStatusCode.InternalServerError) throw new Exception(response.StatusCode.ToString()); diff --git a/SW.Bitween.NativeAdapters/HttpReceiver/adapter.json b/SW.Bitween.NativeAdapters/HttpReceiver/adapter.json new file mode 100644 index 00000000..604bc13a --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpReceiver/adapter.json @@ -0,0 +1,117 @@ +{ + "manifestVersion": 1, + "id": "nativehttpreceiver", + "displayName": "HTTP poll", + "summary": "Polls an HTTP endpoint on a schedule and takes each response as a message.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "http", + "rest", + "api", + "polling" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "AuthType", + "type": "text", + "description": "Authentication type used to call the source endpoint (e.g. ApiKey, Basic, OAuth2, Bearer)." + }, + { + "name": "ApiKey", + "type": "text", + "secret": true, + "description": "API key used for ApiKey or Bearer authentication." + }, + { + "name": "LoginUrl", + "type": "text", + "description": "URL used to obtain a login token for OAuth2 or Basic auth flows." + }, + { + "name": "LoginUsername", + "type": "text", + "description": "Username for Basic or OAuth2 password-grant authentication." + }, + { + "name": "LoginPassword", + "type": "text", + "secret": true, + "description": "Password for Basic or OAuth2 password-grant authentication." + }, + { + "name": "LoginBody", + "type": "text", + "description": "Optional custom login request (Login auth type). Put the real username and password in LoginUsername and LoginPassword, then write {{username}} and {{password}} here where they belong, e.g. {\u0022email\u0022:\u0022{{username}}\u0022,\u0022password\u0022:\u0022{{password}}\u0022}. This field is not hidden, so never type a secret into it directly. Leave empty to send the default body." + }, + { + "name": "LoginTokenPath", + "type": "text", + "description": "Optional path to the token in the login response (e.g. token, data.access_token). Empty reads the \u0027jwt\u0027 field." + }, + { + "name": "Url", + "type": "text", + "required": true, + "default": "", + "description": "The source HTTP endpoint URL to pull data from." + }, + { + "name": "Headers", + "type": "text", + "secret": true, + "description": "Additional HTTP headers as key=value pairs separated by newlines." + }, + { + "name": "ClientId", + "type": "text", + "description": "OAuth2 client ID." + }, + { + "name": "ClientSecret", + "type": "text", + "secret": true, + "description": "OAuth2 client secret." + }, + { + "name": "ContentType", + "type": "text", + "default": "application/json", + "description": "Content-Type header sent with the request (e.g. application/json, application/xml)." + }, + { + "name": "Verb", + "type": "text", + "default": "get", + "description": "HTTP verb to use when pulling data (e.g. get, post)." + }, + { + "name": "DefaultRequest", + "type": "text", + "description": "Default request body template used when no payload is needed." + }, + { + "name": "ArrayPath", + "type": "text", + "description": "JSON path to the array element in the response to iterate over (e.g. $.items)." + }, + { + "name": "TimeoutSeconds", + "type": "number", + "default": "100", + "description": "Seconds to wait for the endpoint to answer before the run fails. Default 100." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/Interfaces.cs b/SW.Bitween.NativeAdapters/Interfaces.cs index a6e7a095..8d53bda7 100644 --- a/SW.Bitween.NativeAdapters/Interfaces.cs +++ b/SW.Bitween.NativeAdapters/Interfaces.cs @@ -11,7 +11,7 @@ public interface INativeAdapter /// /// Marks an adapter that only works with a Rebex license key configured. Such adapters are -/// always registered — the key is a setting that can change at runtime — but they're kept out +/// always registered - the key is a setting that can change at runtime - but they're kept out /// of the adapter pickers while no key is set. /// public interface IRequiresRebexLicense { } @@ -31,7 +31,7 @@ public interface IRequiresRebexLicense { } /// /// /// A mapper marked this way is handed the values separately and the payload is left exactly as it -/// arrived — which is the only way a format other than JSON can be mapped at all. +/// arrived - which is the only way a format other than JSON can be mapped at all. /// /// public interface IReceivesMappingContext { } diff --git a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs index 6fe4a99d..01721e99 100644 --- a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs +++ b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs @@ -17,9 +17,9 @@ public static string Render(string scribanTemplate, string inputJson) var rendered = RenderText(scribanTemplate, inputJson); // 6. Strip trailing commas that may appear after the last field/element - rendered = Regex.Replace(rendered, @",(\s*[}\]])", "$1"); + rendered = StripTrailingCommas(rendered); - // 7. Parse rendered output — root may be an object OR an array + // 7. Parse rendered output - root may be an object OR an array JToken renderedToken; try { @@ -39,13 +39,35 @@ public static string Render(string scribanTemplate, string inputJson) /// without requiring the result to be JSON. /// /// - /// For templates whose output is prose rather than a payload — an email subject or body, say. + /// For templates whose output is prose rather than a payload - an email subject or body, say. /// builds on this and adds the JSON validation and dotted-key expansion /// that a mapper needs and a sentence does not. /// + /// + /// The template filled from a JSON message, or as written when the message isn't JSON: a CSV or a + /// flat file has no fields to fill. A broken template still throws, so a typo isn't used literally. + /// + public static string FillIfJson(string template, string? payload) => + string.IsNullOrEmpty(template) || !LooksLikeJson(payload) ? template : RenderText(template, payload!); + + public static bool LooksLikeJson(string? payload) + { + if (string.IsNullOrWhiteSpace(payload)) return false; + if (payload.TrimStart()[0] is not ('{' or '[')) return false; + try + { + JToken.Parse(payload); + return true; + } + catch (Newtonsoft.Json.JsonReaderException) + { + return false; + } + } + public static string RenderText(string scribanTemplate, string inputJson) { - // 1. Parse input JSON — handle both root object and root array + // 1. Parse input JSON - handle both root object and root array var rootToken = JToken.Parse(inputJson); // 2. Build top-level ScriptObject from input (recursive) @@ -89,8 +111,11 @@ public static string RenderText(string scribanTemplate, string inputJson) System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : (object?)null; })); - // 4. Create template context - var context = new TemplateContext { StrictVariables = false }; + // 4. Create template context. Each loop is already capped (LoopLimit, 1000 by default), + // but loops nest, so three of them are a billion iterations inside the API process - and + // the preview endpoint renders a template the caller writes. The time limit bounds that. + using var timeout = new CancellationTokenSource(RenderTimeout); + var context = new TemplateContext { StrictVariables = false, CancellationToken = timeout.Token }; context.PushGlobal(scriptObj); context.PushGlobal(functions); @@ -102,12 +127,60 @@ public static string RenderText(string scribanTemplate, string inputJson) throw new InvalidOperationException($"Template parse error: {errors}"); } - return template.Render(context); + try + { + return template.Render(context); + } + catch (Scriban.Syntax.ScriptAbortException) when (timeout.IsCancellationRequested) + { + throw new InvalidOperationException( + $"The mapping template took longer than {RenderTimeout.TotalSeconds:0} seconds to render and was stopped."); + } } + /// Far beyond any real mapping; there only to stop a runaway one. + internal static TimeSpan RenderTimeout { get; set; } = TimeSpan.FromSeconds(30); + // ─── Helpers ────────────────────────────────────────────────────────────── - /// Custom | json Scriban pipe — serializes any value to its JSON representation. + /// + /// Drops a comma that is followed only by whitespace and a closing bracket, outside string + /// values. The regex this replaced did not know where strings were, so a value such as + /// "a, }" was rewritten to "a }" on its way to the partner. + /// + internal static string StripTrailingCommas(string json) + { + var output = new System.Text.StringBuilder(json.Length); + var inString = false; + for (var i = 0; i < json.Length; i++) + { + var c = json[i]; + if (inString) + { + output.Append(c); + if (c == '\\' && i + 1 < json.Length) output.Append(json[++i]); + else if (c == '"') inString = false; + continue; + } + + if (c == '"') + { + inString = true; + } + else if (c == ',') + { + var next = i + 1; + while (next < json.Length && char.IsWhiteSpace(json[next])) next++; + if (next < json.Length && json[next] is '}' or ']') continue; + } + + output.Append(c); + } + + return output.ToString(); + } + + /// Custom | json Scriban pipe - serializes any value to its JSON representation. private static string JsonFilter(object? value) { return value switch diff --git a/SW.Bitween.NativeAdapters/JsonMapper/adapter.json b/SW.Bitween.NativeAdapters/JsonMapper/adapter.json new file mode 100644 index 00000000..79535c7e --- /dev/null +++ b/SW.Bitween.NativeAdapters/JsonMapper/adapter.json @@ -0,0 +1,33 @@ +{ + "manifestVersion": 1, + "id": "nativejsonmapper", + "displayName": "JSON mapper", + "summary": "Transforms JSON with a JSON mapping template.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "mapping", + "transform", + "json" + ], + "categories": [ + "Transformation" + ], + "kinds": [ + "mapper" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "ScribanTemplate", + "type": "text", + "required": true, + "default": "{}" + } + ] +} diff --git a/SW.Bitween.NativeAdapters/Mapper/DocumentMapper.cs b/SW.Bitween.NativeAdapters/Mapper/DocumentMapper.cs index 1aaf9f06..388fdc2d 100644 --- a/SW.Bitween.NativeAdapters/Mapper/DocumentMapper.cs +++ b/SW.Bitween.NativeAdapters/Mapper/DocumentMapper.cs @@ -12,7 +12,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// writer, not a change here. /// /// -/// A pure function, which is the point — it is tested by asserting on the tree it returns, with no +/// A pure function, which is the point - it is tested by asserting on the tree it returns, with no /// format and no document text involved. That is only possible because it builds its own neutral /// tree rather than writing into a format's builder as it goes. /// @@ -23,7 +23,7 @@ public static class DocumentMapper /// Where a rule reads from: the entry it sits in, and the document that entry came from. /// /// - /// Both are needed because a rule inside a list may want either — sku from the line, or + /// Both are needed because a rule inside a list may want either - sku from the line, or /// the order's reference from the top of the document. Carrying them together keeps the two /// from drifting apart as they are passed down through nested lists. /// @@ -37,8 +37,8 @@ private readonly record struct Scope(ValueNode? Root, ValueNode? Current) /// What the mapper needs to know about the source document beyond the document itself. /// /// - /// Both are facts about the whole source rather than about any one rule — how the partner - /// writes dates, and whether their format can tell a list of one from a single value — so they + /// Both are facts about the whole source rather than about any one rule - how the partner + /// writes dates, and whether their format can tell a list of one from a single value - so they /// travel together instead of widening four signatures again for the next one. /// private readonly record struct SourceTraits(DateOrder DateOrder, bool SingleValueIsAList); @@ -66,7 +66,7 @@ public static ValueNode Map(MappingRules rules, ValueNode? source, MappingContex ValueNode output; if (rules.Root is not null) { - // A document is a list or an object, never both — so rules for the other shape are + // A document is a list or an object, never both - so rules for the other shape are // not merely unused, they are configuration nobody will ever see take effect. The // editor puts them aside when the shape is switched; rules written by hand can carry // both, and dropping them silently is the kind of missing data nobody goes looking for. @@ -145,8 +145,8 @@ private static void MapInto( /// /// /// Which is why the source entries are matched before anything is built. The number of rows is - /// then the same wherever is read from — the header, a row, - /// or the trailer — rather than a running total that means something different depending on + /// then the same wherever is read from - the header, a row, + /// or the trailer - rather than a running total that means something different depending on /// where it sits. /// /// @@ -172,7 +172,7 @@ private static ListNode BuildList( errors, target, rows.Count); // A trailer belongs on the file whether the source list had a thousand entries, none, or - // was never there at all — a partner expecting a record count still expects to be told + // was never there at all - a partner expecting a record count still expects to be told // that it is zero. foreach (var entry in rule.After) AddEntry(list, entry.Item, entry.Fields, entry.Lists, scope, context, traits, errors, @@ -210,7 +210,7 @@ private static List Matching( // without a word, so where the format cannot say, a single value walks as a list of one. // // Only for a *named* path, though. A document has exactly one root and it is never a - // repeated element, so there is no ambiguity to forgive at `over: ""` — that is a + // repeated element, so there is no ambiguity to forgive at `over: ""` - that is a // deliberate claim that the whole document is a list, and a document that is not one has // no entries rather than one entry that is the whole document. IReadOnlyList items = over switch @@ -243,7 +243,7 @@ private static List Matching( /// object built from and . /// /// - /// Shared by walked and fixed entries, which is the point — the two differ only in what they + /// Shared by walked and fixed entries, which is the point - the two differ only in what they /// read against, so anything that works in one works in the other. /// private static void AddEntry( @@ -330,7 +330,7 @@ private static bool TryResolveField( /// /// A value out of the document being mapped is safe to quote, and quoting it is most of what /// makes the message useful. A partner property or a values-set entry is configuration, and - /// some of it is secret — an adapter password reaching a number field would otherwise be + /// some of it is secret - an adapter password reaching a number field would otherwise be /// written into XchangeResult.Exception, which is stored and shown on the exchange, and /// returned by the preview API. So those are described by length instead. /// @@ -362,7 +362,7 @@ private static string DescribeValue(object? value, ValueSourceKind kind) => /// /// /// Returns false with a null for "does not match", and false with an - /// error for "cannot be compared" — an ordering operator against a value that is not a number. + /// error for "cannot be compared" - an ordering operator against a value that is not a number. /// A missing field does not match and is not an error, so filtering on an optional field skips /// the items that lack it instead of failing the exchange. /// diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/CsvFormat.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/CsvFormat.cs index b936bbfd..9e52fb48 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/CsvFormat.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/CsvFormat.cs @@ -6,13 +6,13 @@ namespace SW.Bitween.NativeAdapters.Mapper.Formats; /// -/// Reads and writes delimited text — comma, semicolon, pipe or tab. +/// Reads and writes delimited text - comma, semicolon, pipe or tab. /// /// /// /// A file in this format is a list of rows, and that is the whole reason it needs so little /// here. A list rule walking "" already means "the document is itself the list", and -/// Root already means "the whole output is a list" — both written for a JSON document that is +/// Root already means "the whole output is a list" - both written for a JSON document that is /// a bare array, both already tested. So the mapper is untouched: this is a reader and a writer, and /// everything between them already exists. /// @@ -23,7 +23,7 @@ namespace SW.Bitween.NativeAdapters.Mapper.Formats; /// it, and it does it because someone asked rather than because something guessed. /// /// -/// An object output — not a list — is written as a header and exactly one row. That falls out of +/// An object output - not a list - is written as a header and exactly one row. That falls out of /// treating a row as an object rather than being a case of its own. /// /// @@ -37,7 +37,7 @@ public class CsvFormat(CsvOptions? options = null) : IDocumentFormat /// No: a file of one row is a list of one, and says so. /// - /// Unlike XML, there is no ambiguity to resolve — a row is a row whether there is one of them or + /// Unlike XML, there is no ambiguity to resolve - a row is a row whether there is one of them or /// a thousand, so nothing has to be tolerated after the fact. /// public bool SingleValueIsAList => false; @@ -47,7 +47,7 @@ public class CsvFormat(CsvOptions? options = null) : IDocumentFormat /// /// /// A row is flat and a mapping is not, so a rule writing to destination.city has to land - /// somewhere. It becomes a column of exactly that name — which is how the editor already shows + /// somewhere. It becomes a column of exactly that name - which is how the editor already shows /// the path, and the only way to produce such a column at all, since the output-field name box /// splits what is typed into it on dots. /// @@ -73,7 +73,7 @@ public ValueNode Read(string text) if (_options.HasHeader) { // A file that is nothing but a header is a list of no rows, which is a true answer - // and not an error — a carrier with nothing to report sends exactly that. + // and not an error - a carrier with nothing to report sends exactly that. if (!parser.Read()) return list; foreach (var name in parser.Record ?? []) names.Add(Unique(name.Length > 0 ? name : Position(names.Count), used)); @@ -117,8 +117,8 @@ public string Write(ValueNode root) "This mapping produced a single value."), }; - // Every column any row has, in the order they first appear. Rows can legitimately differ — - // entries written into a list carry their own rules — and taking the first row's columns + // Every column any row has, in the order they first appear. Rows can legitimately differ - + // entries written into a list carry their own rules - and taking the first row's columns // would drop the rest without a word. var columns = new List(); var seen = new HashSet(StringComparer.Ordinal); @@ -163,7 +163,7 @@ public string Write(ValueNode root) /// /// /// A list inside a row is refused rather than folded. There is no column name that would make - /// ["A1","B7"] fit into one cell, and inventing one — joining them, taking the first — + /// ["A1","B7"] fit into one cell, and inventing one - joining them, taking the first - /// would lose data quietly, which is the one outcome worth refusing over. /// private static void Flatten(ValueNode node, string prefix, Dictionary cells) @@ -175,7 +175,7 @@ private static void Flatten(ValueNode node, string prefix, Dictionary /// Whether to pin the line ending. CsvHelper applies NewLine on reading only when it has /// been set explicitly, and setting it there would refuse a file that separates its rows with - /// bare newlines — which most of them do. So it is pinned for writing, where RFC 4180 asks for + /// bare newlines - which most of them do. So it is pinned for writing, where RFC 4180 asks for /// CRLF, and left alone for reading, where whatever the partner sent has to be accepted. /// private CsvConfiguration Configuration(bool forWriting) @@ -220,7 +220,7 @@ private CsvConfiguration Configuration(bool forWriting) HasHeaderRecord = false, // A partner file is not a well-formed file. A stray quote in the middle of a field is // common enough that refusing the whole document over one would stop a day's shipments, - // and the field still arrives — as the characters that are actually there. + // and the field still arrives - as the characters that are actually there. BadDataFound = null, MissingFieldFound = null, DetectColumnCountChanges = false, @@ -238,7 +238,7 @@ args.Field is not null && args.Field.Contains('\n')), }; - // Assigned only for writing, because assigning it at all is what counts as setting it — + // Assigned only for writing, because assigning it at all is what counts as setting it - // and a reader pinned to one line ending refuses every file that uses the other. if (forWriting) configuration.NewLine = "\r\n"; @@ -277,8 +277,8 @@ private static string Unique(string wanted, HashSet used) /// Whether a record is a blank line rather than a row. /// /// - /// IgnoreBlankLines covers an empty line, but a line holding only delimiters — which the - /// samples have between blocks of records — parses as a row of empty fields. Mapping that would + /// IgnoreBlankLines covers an empty line, but a line holding only delimiters - which the + /// samples have between blocks of records - parses as a row of empty fields. Mapping that would /// produce a row of nothing for every gap in the file. /// private static bool IsBlank(string[] record) => record.All(field => field.Length == 0); @@ -287,7 +287,7 @@ private static string Unique(string wanted, HashSet used) /// Drops the byte-order mark, if the text still carries one. /// /// - /// Excel writes one, and left in place it becomes part of the first column's name — so a header + /// Excel writes one, and left in place it becomes part of the first column's name - so a header /// of ShipmentNumber arrives as ShipmentNumber and every rule reading it /// resolves to nothing, with the editor showing a name that looks exactly right. /// diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/CsvOptions.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/CsvOptions.cs index 8aa0afc9..abdc93a9 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/CsvOptions.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/CsvOptions.cs @@ -29,7 +29,7 @@ public class CsvOptions /// /// /// False is not the unusual case. Two of the three files a single client sends start straight - /// into data, so the fields have no names at all and paths are positions instead — see + /// into data, so the fields have no names at all and paths are positions instead - see /// . /// public bool HasHeader { get; set; } = true; @@ -41,7 +41,7 @@ public class CsvOptions /// /// Three bytes that tell Excel the text is UTF-8. Without them a name like /// BEAUTRAIT Raphaël opens as Raphaël, and whoever opens it has no way to - /// correct that after the fact — which is why this has to be decided when the file is written. + /// correct that after the fact - which is why this has to be decided when the file is written. /// /// /// Off by default, because a partner's own parser can just as easily choke on three bytes it diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/DocumentFormats.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/DocumentFormats.cs index e0c3340e..4f91ba5b 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/DocumentFormats.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/DocumentFormats.cs @@ -6,7 +6,7 @@ namespace SW.Bitween.NativeAdapters.Mapper.Formats; /// The formats a mapping can read and write, by the id stored in its rules. /// /// -/// One registry, so the adapter and the preview endpoint cannot disagree about what is supported — +/// One registry, so the adapter and the preview endpoint cannot disagree about what is supported - /// a format the editor offers but the pipeline rejects would be a mapping that previews and then /// fails. Adding a format is one entry here plus the reader and writer it names. /// @@ -19,8 +19,8 @@ public static class DocumentFormats /// How each id is built. /// /// - /// Factories rather than instances because delimited text is configured per side — a partner's - /// semicolon file routinely becomes somebody else's comma file — so its reader and its writer + /// Factories rather than instances because delimited text is configured per side - a partner's + /// semicolon file routinely becomes somebody else's comma file - so its reader and its writer /// cannot be the same shared object. The formats that need no configuration stay singletons. /// private static readonly Dictionary> ById = diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/IDocumentFormat.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/IDocumentFormat.cs index 83f13a9c..2d5554d3 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/IDocumentFormat.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/IDocumentFormat.cs @@ -16,7 +16,7 @@ public interface IDocumentFormat /// What to report for a document this format produced. /// /// Set on the output XchangeFile. The previous mapper left it unset, and the gateway falls - /// back to application/json — so an XML document would have been served to a partner as + /// back to application/json - so an XML document would have been served to a partner as /// JSON. /// string ContentType { get; } @@ -25,7 +25,7 @@ public interface IDocumentFormat /// Whether a single value has to be walked as a list of one. /// /// - /// A format that makes a list by repeating a name — XML — cannot tell a list of one from a + /// A format that makes a list by repeating a name - XML - cannot tell a list of one from a /// value that was never a list, so a list rule over such a path must walk what it found rather /// than treat it as no entries at all. JSON says which, so it says no. /// diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/JsonFormat.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/JsonFormat.cs index 261e36fa..1132536e 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/JsonFormat.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/JsonFormat.cs @@ -32,7 +32,7 @@ public ValueNode Read(string text) try { // FloatParseHandling.Decimal so a price arrives exact. The default gives double, and - // 1.16 has no exact binary representation — the arithmetic downstream is money. + // 1.16 has no exact binary representation - the arithmetic downstream is money. using var reader = new JsonTextReader(new StringReader(text)) { FloatParseHandling = FloatParseHandling.Decimal, @@ -42,7 +42,7 @@ public ValueNode Read(string text) // ReadFrom stops at the end of the first value, so two documents concatenated would // read as the first one and the rest would be dropped without a word. Reading one more - // token settles it — the reader itself objects to trailing content, and a token that + // token settles it - the reader itself objects to trailing content, and a token that // does arrive means there is a second value. if (reader.Read()) throw new DocumentFormatException( @@ -91,8 +91,8 @@ private static ValueNode FromToken(JToken token) /// /// Newtonsoft hands back long for a whole number, so without this the same rule would /// behave differently for 1 and 1.5. One numeric type through the mapper means one - /// set of conversion rules to test. A value too large for decimal — only reachable through - /// scientific notation — is kept as text rather than silently losing precision. + /// set of conversion rules to test. A value too large for decimal - only reachable through + /// scientific notation - is kept as text rather than silently losing precision. /// private static object? Normalise(object? value) => value switch { @@ -104,7 +104,7 @@ private static ValueNode FromToken(JToken token) ? (decimal)d : d.ToString("R", CultureInfo.InvariantCulture), float f => (decimal)f, - // An integer past long's range — a 20-digit reference, say — comes back as BigInteger. + // An integer past long's range - a 20-digit reference, say - comes back as BigInteger. System.Numerics.BigInteger big => big >= new System.Numerics.BigInteger(decimal.MinValue) && big <= new System.Numerics.BigInteger(decimal.MaxValue) diff --git a/SW.Bitween.NativeAdapters/Mapper/Formats/XmlFormat.cs b/SW.Bitween.NativeAdapters/Mapper/Formats/XmlFormat.cs index 3935b215..1ce3c9f3 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Formats/XmlFormat.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Formats/XmlFormat.cs @@ -16,7 +16,7 @@ namespace SW.Bitween.NativeAdapters.Mapper.Formats; /// /// Namespaces are dropped on the way in and matched on the local name alone. Real documents bind /// the same URI to a prefix and to the default at once, un-declare it again further down with -/// xmlns="", and pick a different prefix for the same namespace in the very next element — +/// xmlns="", and pick a different prefix for the same namespace in the very next element - /// so a path that named a prefix would be naming something the partner is free to change. /// /// @@ -38,14 +38,14 @@ public class XmlFormat : IDocumentFormat /// /// XML repeats a name to make a list, so a document with one <line> is /// indistinguishable from one that is not a list at all. Without this, an order that happened to - /// have a single line would map to no lines — the line silently gone, and nothing to say so. + /// have a single line would map to no lines - the line silently gone, and nothing to say so. /// public bool SingleValueIsAList => true; /// How deep a document may nest before it is refused. /// /// recurses once per element, and a document nested thousands deep - /// would exhaust the stack — which no catch can recover from, so the process goes with + /// would exhaust the stack - which no catch can recover from, so the process goes with /// it. has no nesting limit of its own to lean on. 64 is what /// Newtonsoft applies to JSON, so both formats refuse the same shape, and it is far past /// anything a real document reaches: a SOAP envelope carrying an order gets to about ten. @@ -80,7 +80,7 @@ public ValueNode Read(string text) throw new DocumentFormatException("The document has no root element."); // The root element is a named key rather than the tree itself, so its name is part of every - // path — `Envelope.Body.shipping.height`. It is the one name in an XML document that says + // path - `Envelope.Body.shipping.height`. It is the one name in an XML document that says // what the document is, and a mapping that never mentions it would be harder to read. var root = ValueNode.Object(); root.Set(document.Root.Name.LocalName, FromElement(document.Root, 1)); @@ -164,7 +164,7 @@ private static XElement BuildElement(string name, ValueNode? node, NamespaceScop break; // A scalar is the element's text. Null and "" both give ``, which is the same - // document either way — XML has no null, and an element with no text is what it has. + // document either way - XML has no null, and an element with no text is what it has. case ScalarNode scalar: var text = AsText(scalar.Value); if (text.Length > 0) element.Add(new XText(text)); @@ -220,7 +220,7 @@ private static void Add(XElement parent, string key, ValueNode child, NamespaceS /// /// /// xmlns="" puts a subtree back into no namespace, and real documents do it for every - /// section of a request body. It cannot be written as an attribute — the writer objects — but it + /// section of a request body. It cannot be written as an attribute - the writer objects - but it /// does not need to be: an element built with no namespace under a parent that has one makes the /// writer emit xmlns="" itself. /// @@ -242,7 +242,7 @@ private static bool IsNamespaceKey(string name) => /// Resolves a written name against the prefixes in scope. /// /// - /// An unprefixed attribute is in no namespace even where a default namespace is in scope — the + /// An unprefixed attribute is in no namespace even where a default namespace is in scope - the /// one place XML treats elements and attributes differently, and the reason this takes a flag. /// private static XName ResolveName(string name, NamespaceScope scope, bool isAttribute) @@ -357,7 +357,7 @@ private static ValueNode FromElement(XElement element, int depth) var children = element.Elements().ToList(); // Nothing but text: the element is its value. `` and `` are the same document, so - // both read as "" — present and empty, which is what the element says, and distinguishable + // both read as "" - present and empty, which is what the element says, and distinguishable // from a missing element, which reads as nothing at all. if (attributes.Count == 0 && children.Count == 0) return ValueNode.Value(element.Value); @@ -383,7 +383,7 @@ private static ValueNode FromElement(XElement element, int depth) node.Set(group.Key, list); } - // Text alongside children — mixed content. Rare in the documents a partner sends, but + // Text alongside children - mixed content. Rare in the documents a partner sends, but // dropping it would lose data with no error, which is the one outcome to avoid. var text = string.Concat(element.Nodes().OfType().Select(t => t.Value)); if (!string.IsNullOrWhiteSpace(text)) node.Set(TextKey, ValueNode.Value(text)); diff --git a/SW.Bitween.NativeAdapters/Mapper/MappingContext.cs b/SW.Bitween.NativeAdapters/Mapper/MappingContext.cs index e91ee83b..c7d7ee31 100644 --- a/SW.Bitween.NativeAdapters/Mapper/MappingContext.cs +++ b/SW.Bitween.NativeAdapters/Mapper/MappingContext.cs @@ -4,8 +4,8 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// The values a mapping can use that do not come out of the document being mapped. /// /// -/// Passed alongside the payload rather than written into it. The previous approach — writing -/// __partner__ and __globals__ keys into the incoming JSON — meant the payload had to +/// Passed alongside the payload rather than written into it. The previous approach - writing +/// __partner__ and __globals__ keys into the incoming JSON - meant the payload had to /// be parsed as JSON before anything knew which mapper was configured, so a non-JSON document could /// not be mapped at all; a document with a real key of the same name lost it; and only a JSON object /// was enriched, so the same mapping behaved differently for a root array. @@ -21,8 +21,8 @@ public class MappingContext new Dictionary>(); /// - /// Values read from the original document — the input of the exchange whose delivery got the - /// response being mapped — by path. Empty + /// Values read from the original document - the input of the exchange whose delivery got the + /// response being mapped - by path. Empty /// unless a response or bus gateway subscription is being mapped. /// public IReadOnlyDictionary Source { get; init; } = @@ -78,7 +78,7 @@ public MappingFailedException(IReadOnlyList errors) private static string BuildMessage(IReadOnlyList errors) { var count = errors.Count == 1 ? "1 rule" : $"{errors.Count} rules"; - return $"Mapping failed — {count} could not be applied: " + + return $"Mapping failed - {count} could not be applied: " + string.Join("; ", errors.Select(e => e.ToString())); } } diff --git a/SW.Bitween.NativeAdapters/Mapper/MappingRules.cs b/SW.Bitween.NativeAdapters/Mapper/MappingRules.cs index 4f2949c2..4cd55903 100644 --- a/SW.Bitween.NativeAdapters/Mapper/MappingRules.cs +++ b/SW.Bitween.NativeAdapters/Mapper/MappingRules.cs @@ -3,7 +3,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// -/// What the user configured, written down. The source of truth for a mapping — nothing is derived +/// What the user configured, written down. The source of truth for a mapping - nothing is derived /// from a generated artefact, and nothing is inferred from a sample document. /// /// @@ -24,7 +24,7 @@ public class MappingRules public int Version { get; set; } = CurrentVersion; - /// Format id of the incoming document — see IDocumentFormat.Id. + /// Format id of the incoming document - see IDocumentFormat.Id. public string SourceFormat { get; set; } = "json"; /// Format id of the document to produce. @@ -36,7 +36,7 @@ public class MappingRules /// /// /// On the mapping rather than on each rule, because a partner writes dates one way - /// throughout a document — asking per field would be the same answer typed many times, + /// throughout a document - asking per field would be the same answer typed many times, /// and one row set wrongly would be invisible. /// /// @@ -66,13 +66,13 @@ public class MappingRules /// When set, the whole output document is this list rather than an object. /// /// - /// Some partners expect a bare array — [ {...}, {...} ] — rather than an object with a + /// Some partners expect a bare array - [ {...}, {...} ] - rather than an object with a /// list inside it. and are ignored when this is set, /// because a document is one thing or the other. /// public ListRule? Root { get; set; } - /// Every field rule, wherever it sits — at the top, in a list, or in a written-in entry. + /// Every field rule, wherever it sits - at the top, in a list, or in a written-in entry. public IEnumerable EveryField() { var lists = new Stack(Lists); @@ -103,7 +103,7 @@ public class FieldRule /// /// /// Segments rather than a dotted string, because a dotted string has to be split and then a key - /// that genuinely contains a dot becomes unreachable — the old mapper turned a target of + /// that genuinely contains a dot becomes unreachable - the old mapper turned a target of /// file.txt into a nested { "file": { "txt": … } } with no way to ask for the /// literal key. /// @@ -111,7 +111,7 @@ public class FieldRule public ValueSource From { get; set; } = new(); - /// Optional named function applied to the value — see . + /// Optional named function applied to the value - see . public TransformRule? Transform { get; set; } /// Optional value substitution applied after . @@ -122,7 +122,7 @@ public class FieldRule /// /// /// On the rule, not inferred from a target sample document. The old mapper read types out of - /// whatever JSON was pasted into the editor, which meant the sample changed the output — so the + /// whatever JSON was pasted into the editor, which meant the sample changed the output - so the /// same rules produced different documents depending on what someone had pasted. /// public ValueType? Type { get; set; } @@ -171,7 +171,7 @@ public enum ValueSourceKind /// /// /// Only different from inside a list: it is how one value from the - /// document — an order reference, say — gets written onto every line of that order. + /// document - an order reference, say - gets written onto every line of that order. /// RootPath, @@ -190,7 +190,7 @@ public enum ValueSourceKind /// above it, and nothing else in a mapping knows that number. /// /// - /// Entries written by hand are not counted, which is the whole point of the distinction — a + /// Entries written by hand are not counted, which is the whole point of the distinction - a /// trailer says how many records there are, and the header line above it is not one. /// Counting them would also mean adding a header later quietly changed the trailer, with /// nothing on screen to say the number had moved. @@ -204,7 +204,7 @@ public enum ValueSourceKind /// /// A path into the original document: the input of the exchange whose delivery got the response - /// being mapped — the order, when the response is the carrier's reply to it. Shown as "Original". + /// being mapped - the order, when the response is the carrier's reply to it. Shown as "Original". /// /// /// Only a response or bus gateway subscription is handed these, read when its exchange was @@ -264,7 +264,7 @@ public class LookupRule /// A list in the output, whose entries come from walking a list in the source. /// -/// Named for what it produces rather than for how — a list, not a loop. Walking a source list is +/// Named for what it produces rather than for how - a list, not a loop. Walking a source list is /// one way to fill one, and the name leaves room for the others without becoming a lie. /// public class ListRule @@ -291,7 +291,7 @@ public class ListRule public FilterRule? Where { get; set; } /// - /// When set, each entry produces a single value instead of an object — a list of strings or + /// When set, each entry produces a single value instead of an object - a list of strings or /// numbers rather than a list of records. /// /// @@ -308,7 +308,7 @@ public class ListRule /// /// /// Nesting is structural rather than a flat list joined by parent ids, which is how the old - /// model did it — that needed three separate recursive helpers to rebuild the full source and + /// model did it - that needed three separate recursive helpers to rebuild the full source and /// target paths, and a rule whose parent had been deleted became unreachable rather than /// invalid. /// @@ -324,7 +324,7 @@ public class ListRule /// /// /// Each entry is built from its own rules, so a constant entry may still read the document, - /// the partner or a values set, and may contain lists of its own — where the old mapper had + /// the partner or a values set, and may contain lists of its own - where the old mapper had /// two separate features holding literal JSON and a flat array of value slots. /// /// @@ -336,7 +336,7 @@ public class ListRule /// /// The other end of , and the reason it exists: a delimited file from a /// carrier ends with a trailer record carrying the number of records above it. Built exactly - /// the same way, reading against the same scope — the only difference is where they land, and + /// the same way, reading against the same scope - the only difference is where they land, and /// that can see the whole list by the time they run. /// public List After { get; set; } = new(); @@ -346,7 +346,7 @@ public class ListRule public class ListEntry { /// - /// When set, the entry is a single value rather than an object — the same choice + /// When set, the entry is a single value rather than an object - the same choice /// makes for a walked entry. /// public FieldRule? Item { get; set; } diff --git a/SW.Bitween.NativeAdapters/Mapper/NativeMapper.cs b/SW.Bitween.NativeAdapters/Mapper/NativeMapper.cs index 15c90b7d..6af90991 100644 --- a/SW.Bitween.NativeAdapters/Mapper/NativeMapper.cs +++ b/SW.Bitween.NativeAdapters/Mapper/NativeMapper.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// Settings the mapper is configured with. /// /// is the whole mapping. There is no template setting, because nothing is -/// generated — the rules are read and applied directly. +/// generated - the rules are read and applied directly. /// public class NativeMapperInput { @@ -115,7 +115,7 @@ private static MappingRules ReadRules(IDictionary settings) /// Reads the context the pipeline passed, or an empty one. /// /// - /// Absent context is not an error — a mapping with no partner rules and no global rules needs + /// Absent context is not an error - a mapping with no partner rules and no global rules needs /// none, and a preview may run without one. A rule that then asks for a partner value gets null, /// which is the same answer it would get for a partner with no such property. /// diff --git a/SW.Bitween.NativeAdapters/Mapper/Transforms.cs b/SW.Bitween.NativeAdapters/Mapper/Transforms.cs index 6d3d61b5..7a45dbaa 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Transforms.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Transforms.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// /// /// A closed list rather than a free-text expression, which means there is no expression language to -/// parse, sandbox or generate — and the editor can offer a dropdown instead of a syntax to learn. +/// parse, sandbox or generate - and the editor can offer a dropdown instead of a syntax to learn. /// Each function here is an ordinary method with its own test. /// /// @@ -42,7 +42,7 @@ public static bool TryApply(TransformRule rule, object? value, DateOrder dateOrd // that: it aborts the run at the first bad rule and surfaces as a stack trace instead. // // Arguments come from a saved rule and are not validated on the way in, so a few of them - // throw rather than return — an out-of-range decimal cast for a substring index, a decimal + // throw rather than return - an out-of-range decimal cast for a substring index, a decimal // multiplication that overflows, a date format string the framework rejects. Those become // ordinary failures here. try @@ -119,7 +119,7 @@ private static bool Apply(TransformRule rule, object? value, DateOrder dateOrder case "replace": { var find = ArgAsString(rule, "find"); - // Replacing the empty string is not a no-op — String.Replace throws for it — and + // Replacing the empty string is not a no-op - String.Replace throws for it - and // there is no sensible reading of "replace nothing with something". if (string.IsNullOrEmpty(find)) { @@ -283,8 +283,8 @@ private static bool TryReadDate(string text, DateOrder order, out DateTimeOffset /// A value as text, for the string functions. /// /// - /// Uses the same spellings does — round-trip for numbers, and - /// lowercase for booleans — so upper on a number cannot produce a different string than + /// Uses the same spellings does - round-trip for numbers, and + /// lowercase for booleans - so upper on a number cannot produce a different string than /// a string target would have. /// private static string AsString(object value) => diff --git a/SW.Bitween.NativeAdapters/Mapper/ValueNode.cs b/SW.Bitween.NativeAdapters/Mapper/ValueNode.cs index c08948d3..344dd067 100644 --- a/SW.Bitween.NativeAdapters/Mapper/ValueNode.cs +++ b/SW.Bitween.NativeAdapters/Mapper/ValueNode.cs @@ -13,7 +13,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// /// /// Values stay as , , or null right up -/// until a writer serialises them — never as text. That is the whole reason this type exists: the +/// until a writer serialises them - never as text. That is the whole reason this type exists: the /// old mapper rendered a text template and then had to quote, escape and repair the result, and /// escaping is only a problem for something that has already become text. /// @@ -48,8 +48,8 @@ public sealed class ScalarNode(object? value) : ValueNode /// A named set of children, in insertion order. /// /// -/// Order is kept because XML cares about it — an XSD sequence is only valid in the order it declares -/// — and because a JSON document whose keys come out in a different order every run is unpleasant to +/// Order is kept because XML cares about it - an XSD sequence is only valid in the order it declares +/// - and because a JSON document whose keys come out in a different order every run is unpleasant to /// diff. The rules decide the order; nothing infers it from a sample document. /// public sealed class ObjectNode : ValueNode @@ -78,7 +78,7 @@ public void Set(string key, ValueNode child) /// /// Replaces a scalar sitting in the way, which happens when one rule targets a and /// another targets a.b. Last writer wins, and validation is what should catch the - /// conflict — silently dropping the deeper rule would be worse. + /// conflict - silently dropping the deeper rule would be worse. /// public ObjectNode GetOrAddObject(string key) { diff --git a/SW.Bitween.NativeAdapters/Mapper/Values.cs b/SW.Bitween.NativeAdapters/Mapper/Values.cs index f9c648e5..2d7189b3 100644 --- a/SW.Bitween.NativeAdapters/Mapper/Values.cs +++ b/SW.Bitween.NativeAdapters/Mapper/Values.cs @@ -8,7 +8,7 @@ namespace SW.Bitween.NativeAdapters.Mapper; /// /// /// Both of these used to be emitted into a Scriban template as generated code, because the old -/// generator ran at save time when no value existed yet — so all it could do was write down +/// generator ran at save time when no value existed yet - so all it could do was write down /// instructions to run later, in the only language available. A number cast came out as a /// 200-character nest of object.typeof ternaries that could not be tested on its own. Here /// they are ordinary methods, run at the moment the value exists. @@ -28,7 +28,7 @@ public static class Values /// /// An order reference of 42 must not become "42.0", and a quantity must not come out as "4E+01". /// Trailing zeros are dropped because decimal keeps the scale of the arithmetic that produced - /// the value — 116.00 from a multiply would otherwise reach the document with both zeros. + /// the value - 116.00 from a multiply would otherwise reach the document with both zeros. /// public static string FormatNumber(decimal value) { @@ -42,13 +42,13 @@ public static string FormatNumber(decimal value) /// /// Null converts to null for every type: a missing source field is not a failure, it is an /// absent value, and the writer decides how to represent that. A conversion that cannot - /// be done — the string "abc" as a number — returns false so the caller can report which rule + /// be done - the string "abc" as a number - returns false so the caller can report which rule /// it was, rather than quietly substituting null. /// public static bool TryCoerce(object? value, ValueType? type, out object? result) { - // Values written into the rules — a fixed value, a filter's comparand, a lookup table entry - // — are deserialised as `object`, so a whole number arrives as long and a fraction as + // Values written into the rules - a fixed value, a filter's comparand, a lookup table entry + // - are deserialised as `object`, so a whole number arrives as long and a fraction as // double. Reduce every CLR numeric to decimal first, or a rule holding 0 would fail to // convert to a number while a document holding 0 succeeded. value = NormaliseNumeric(value); @@ -77,7 +77,7 @@ public static bool TryCoerce(object? value, ValueType? type, out object? result) case bool b: result = b ? 1m : 0m; return true; - case string s when decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed): + case string s when TryParseNumber(s, out var parsed): result = parsed; return true; default: @@ -116,7 +116,7 @@ public static bool TryCoerce(object? value, ValueType? type, out object? result) /// Reduces any CLR numeric to , leaving everything else alone. /// /// - /// A value too large for decimal — reachable only through scientific notation — is left as the + /// A value too large for decimal - reachable only through scientific notation - is left as the /// double it was, so it fails a numeric conversion loudly rather than silently wrapping. /// private static object? NormaliseNumeric(object? value) => value switch @@ -134,7 +134,7 @@ public static bool TryCoerce(object? value, ValueType? type, out object? result) /// The spellings of true and false that appear in real documents. /// /// - /// XML and CSV have no types, so a boolean arrives as whatever the sender writes — and + /// XML and CSV have no types, so a boolean arrives as whatever the sender writes - and /// xs:boolean itself permits both true/false and 1/0. /// Anything outside this list is a failure rather than a guess. /// @@ -159,7 +159,7 @@ private static bool TryParseBoolean(string text, out bool value) /// /// /// - /// Returns the node so the caller can tell a list apart from a value — a list rule needs the list, a + /// Returns the node so the caller can tell a list apart from a value - a list rule needs the list, a /// field needs the value. A path that does not exist gives null, which is an absent value and /// not an error: optional fields are ordinary. /// @@ -202,4 +202,30 @@ public static void PlaceAt(ObjectNode root, IReadOnlyList target, ValueN current.Set(target[^1], value); } + + /// + /// A number written in invariant form: an optional sign, currency symbol or accounting + /// parentheses, and commas only where thousands separators belong. + /// + /// + /// NumberStyles.Any ignores where a comma sits, so "1,5" - one and a half, as half the + /// world writes it - became fifteen with no error, and a partner got ten times the amount. + /// A comma now has to separate groups of three digits ahead of the decimal point; anything + /// else is not a number, and the mapping says so instead of guessing. + /// + internal static bool TryParseNumber(string text, out decimal number) + { + number = 0; + if (text.Contains(',')) + { + var integerPart = System.Text.RegularExpressions.Regex.Match(text, @"\d[\d,]*"); + var afterInteger = text[(integerPart.Index + integerPart.Length)..]; + if (!integerPart.Success || + !System.Text.RegularExpressions.Regex.IsMatch(integerPart.Value, @"^\d{1,3}(,\d{3})+$") || + afterInteger.Contains(',')) + return false; + } + + return decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out number); + } } diff --git a/SW.Bitween.NativeAdapters/Mapper/adapter.json b/SW.Bitween.NativeAdapters/Mapper/adapter.json new file mode 100644 index 00000000..87a8f8f5 --- /dev/null +++ b/SW.Bitween.NativeAdapters/Mapper/adapter.json @@ -0,0 +1,47 @@ +{ + "manifestVersion": 1, + "id": "nativemapper", + "displayName": "Mapping rules", + "summary": "Transforms the message with mapping rules built in Bitween\u0027s mapping editor.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "mapping", + "transform", + "json", + "xml", + "csv" + ], + "categories": [ + "Transformation" + ], + "kinds": [ + "mapper" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "MappingRules", + "type": "text", + "default": "", + "description": "The mapping rules, as JSON. Written by the mapping editor." + }, + { + "name": "SourceSample", + "type": "text", + "default": "", + "description": "Sample source document, kept for the editor. Never read while mapping." + }, + { + "name": "TargetSample", + "type": "text", + "default": "", + "description": "Sample target document, kept for the editor. Never read while mapping." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs b/SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs index e060a05d..308f14f0 100644 --- a/SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs +++ b/SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs @@ -6,21 +6,60 @@ namespace SW.Bitween.NativeAdapters.Pop3Receiver; -public class NativePop3Receiver : INativeInfolinkReceiver +public class NativePop3Receiver : INativeInfolinkReceiver, IDisposable { private Pop3ReceiverInput _options = new(); private Pop3Client? _client; - // Connection defaults matching the standard implicit-SSL POP3 endpoint. - // Not user-configurable; exposed internally only so tests can point at a local fake server. - internal int Port { get; set; } = 995; - internal bool UseSsl { get; set; } = true; + // For tests pointing at a local fake server: when set, these win over the settings. + internal int? Port { get; set; } + internal bool? UseSsl { get; set; } + /// + /// Implicit TLS on 995 unless the settings say otherwise, as it always was. With TLS, the server's + /// certificate must be valid or, when a thumbprint is pinned, be exactly that certificate: how a + /// mail server with its own certificate is trusted. Either way it's checked before the password goes. + /// public async Task Initialize() { + var security = UseSsl switch + { + true => SecureSocketOptions.SslOnConnect, + false => SecureSocketOptions.None, + null => (_options.Encryption ?? "").Trim().ToLowerInvariant() switch + { + "" or "implicit" => SecureSocketOptions.SslOnConnect, + "explicit" => SecureSocketOptions.StartTls, + // No unencrypted option: the receiver always signs in with a password. + _ => throw new ArgumentException($"Unknown Encryption '{_options.Encryption}': use implicit or explicit."), + }, + }; + var port = Port ?? _options.Port ?? (security == SecureSocketOptions.SslOnConnect ? 995 : 110); + _client = new Pop3Client(); - var sslOptions = UseSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None; - await _client.ConnectAsync(_options.Host, Port, sslOptions); + string? refused = null; + if (!string.IsNullOrWhiteSpace(_options.CertificateThumbprint)) + { + var pinned = FtpTransfer.NormalizeThumbprint(_options.CertificateThumbprint); + _client.ServerCertificateValidationCallback = (_, certificate, _, _) => + { + var presented = certificate == null ? "" : FtpTransfer.Thumbprint(certificate); + if (presented == pinned) return true; + refused = presented; + return false; + }; + } + + try + { + await _client.ConnectAsync(_options.Host, port, security); + } + catch (Exception) when (refused is not null) + { + throw new InvalidOperationException( + $"The POP3 server's certificate (SHA-256 {refused}) is not the one configured. Refusing to send credentials to it."); + } + await _client.AuthenticateAsync(_options.Username, _options.Password); } @@ -28,6 +67,7 @@ public async Task Finalize() { await _client!.DisconnectAsync(true); _client.Dispose(); + _client = null; } public Task> ListFiles() @@ -46,7 +86,7 @@ public async Task GetFile(string fileId) return new XchangeFile(message.TextBody ?? message.HtmlBody ?? string.Empty, message.Subject); using var memoryStream = new MemoryStream(); - // A part can carry no content at all — a malformed or truncated message. Treating that + // A part can carry no content at all - a malformed or truncated message. Treating that // as an empty attachment beats a NullReferenceException from inside the receive loop. if (attachment is MessagePart { Message: { } embedded }) await embedded.WriteToAsync(memoryStream); @@ -77,4 +117,14 @@ public void InitializeStartupValues(IDictionary settings) } public Type StartupValuesType => typeof(Pop3ReceiverInput); + + /// + /// Closes a connection a run left open - one that failed before Finalize. Without the QUIT that + /// Finalize sends, the server keeps the messages this run marked for deletion. + /// + public void Dispose() + { + try { _client?.Dispose(); } catch { /* closing only */ } + _client = null; + } } diff --git a/SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs b/SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs index 3df4a0ea..90ac6f83 100644 --- a/SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs +++ b/SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs @@ -18,6 +18,16 @@ public class Pop3ReceiverInput [Description("POP3 account password.")] public string Password { get; set; } = string.Empty; + [DefaultValue("implicit")] + [Description("implicit for POP3 over TLS from the first byte (usually port 995), or explicit for POP3 that upgrades the connection with STLS (usually port 110). The password is never sent unencrypted.")] + public string Encryption { get; set; } = "implicit"; + + [Description("The server's port. 995 with implicit TLS, 110 with explicit, when empty.")] + public int? Port { get; set; } + + [Description("The SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before credentials are sent. When empty, the certificate must be valid.")] + public string? CertificateThumbprint { get; set; } + [DefaultValue(50)] [Description("Maximum number of messages to fetch per polling batch.")] public int BatchSize { get; set; } = 50; diff --git a/SW.Bitween.NativeAdapters/Pop3Receiver/adapter.json b/SW.Bitween.NativeAdapters/Pop3Receiver/adapter.json new file mode 100644 index 00000000..8cbe8136 --- /dev/null +++ b/SW.Bitween.NativeAdapters/Pop3Receiver/adapter.json @@ -0,0 +1,75 @@ +{ + "manifestVersion": 1, + "id": "nativepop3receiver", + "displayName": "Email pickup (POP3)", + "summary": "Reads messages from a POP3 mailbox on a schedule.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "email", + "pop3" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "POP3 server hostname." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "POP3 account username." + }, + { + "name": "Password", + "type": "text", + "required": true, + "secret": true, + "description": "POP3 account password." + }, + { + "name": "Encryption", + "type": "text", + "default": "implicit", + "description": "implicit for POP3 over TLS from the first byte (usually port 995), or explicit for POP3 that upgrades the connection with STLS (usually port 110). The password is never sent unencrypted." + }, + { + "name": "Port", + "type": "number", + "description": "The server's port. 995 with implicit TLS, 110 with explicit, when empty." + }, + { + "name": "CertificateThumbprint", + "type": "text", + "description": "The SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before credentials are sent. When empty, the certificate must be valid." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of messages to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode message contents (e.g. utf8)." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs b/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs index fb054498..084b5f93 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpReceiver/NativeRebexFtpReceiver.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.NativeAdapters.RebexFtpReceiver; -public class NativeRebexFtpReceiver(string? licenseKey = null) : INativeInfolinkReceiver, IRequiresRebexLicense +public class NativeRebexFtpReceiver(string? licenseKey = null) : INativeInfolinkReceiver, IRequiresRebexLicense, IDisposable { private RebexFtpReceiverInput _options = new(); private IFtp _ftpOrSftp = null!; @@ -20,6 +20,7 @@ public async Task Initialize() case "sftpssh": var sftpssh = new Sftp(); await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22); + FtpProtocol.VerifyHostKey(sftpssh, _options.HostKeyFingerprint); var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey)); var privateKey = new SshPrivateKey(keyBytes, _options.Password); @@ -31,6 +32,7 @@ public async Task Initialize() case "sftp": var sftp = new Sftp(); await sftp.ConnectAsync(_options.Host, _options.Port ?? 22); + FtpProtocol.VerifyHostKey(sftp, _options.HostKeyFingerprint); _ftpOrSftp = sftp; await _ftpOrSftp.LoginAsync(_options.Username, _options.Password); break; @@ -54,14 +56,27 @@ public async Task Finalize() { await _ftpOrSftp.DisconnectAsync(); _ftpOrSftp.Dispose(); + _ftpOrSftp = null!; + } + + /// Closes a connection a run left open - one that failed before Finalize. + public void Dispose() + { + try { _ftpOrSftp?.Dispose(); } catch { /* closing only */ } + _ftpOrSftp = null!; } public async Task> ListFiles() { var files = await _ftpOrSftp.GetListAsync(); + // A file still being uploaded is listed like any other; taking it half-written sends half a + // document on, then deletes the rest as it arrives. Left alone until it has been still for a while. + var settledBefore = DateTime.Now.AddSeconds(-_options.MinimumFileAgeSeconds); + return files .Where(i => i.IsFile) + .Where(i => _options.MinimumFileAgeSeconds <= 0 || i.LastWriteTime is not { } written || written <= settledBefore) .Take(_options.BatchSize) .Select(i => i.Name) .ToList(); diff --git a/SW.Bitween.NativeAdapters/RebexFtpReceiver/RebexFtpReceiverInput.cs b/SW.Bitween.NativeAdapters/RebexFtpReceiver/RebexFtpReceiverInput.cs index 290e22da..1b6670c4 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpReceiver/RebexFtpReceiverInput.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpReceiver/RebexFtpReceiverInput.cs @@ -45,4 +45,10 @@ public class RebexFtpReceiverInput [Secure] [Description("Private key used for SFTP authentication instead of a password.")] public string? PrivateKey { get; set; } + + [Description("SHA-256 fingerprint of the SFTP server's host key, as 'ssh-keygen -lf' prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent.")] + public string? HostKeyFingerprint { get; set; } + + [Description("Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed.")] + public int MinimumFileAgeSeconds { get; set; } } diff --git a/SW.Bitween.NativeAdapters/RebexFtpReceiver/adapter.json b/SW.Bitween.NativeAdapters/RebexFtpReceiver/adapter.json new file mode 100644 index 00000000..4aeb86b1 --- /dev/null +++ b/SW.Bitween.NativeAdapters/RebexFtpReceiver/adapter.json @@ -0,0 +1,103 @@ +{ + "manifestVersion": 1, + "id": "nativerebexftpreceiver", + "displayName": "FTP / SFTP pickup (Rebex)", + "summary": "Picks up files from an FTP or SFTP server on a schedule, leaving files still being written for the next run. Uses the Rebex client and needs a Rebex license.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "ftp", + "sftp", + "file" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "FTP/SFTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, the protocol\u0027s default port is used." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "FTP/SFTP account username." + }, + { + "name": "Password", + "type": "text", + "secret": true, + "description": "Account password, or the private key passphrase when using SFTP with a private key." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to change into before listing/downloading files." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of files to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode file contents (e.g. utf8)." + }, + { + "name": "DeleteMovesFileTo", + "type": "text", + "description": "Folder to move a file to after it\u0027s received. If empty, the file is deleted instead." + }, + { + "name": "Protocol", + "type": "text", + "default": "sftp", + "description": "Transfer protocol to use (sftp or ftp)." + }, + { + "name": "CheckFileExistence", + "type": "boolean", + "default": "True", + "description": "Whether to verify the file still exists before attempting to delete or move it after receiving." + }, + { + "name": "PrivateKey", + "type": "text", + "secret": true, + "description": "Private key used for SFTP authentication instead of a password." + }, + { + "name": "HostKeyFingerprint", + "type": "text", + "description": "SHA-256 fingerprint of the SFTP server\u0027s host key, as \u0027ssh-keygen -lf\u0027 prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent." + }, + { + "name": "MinimumFileAgeSeconds", + "type": "number", + "default": "0", + "description": "Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs index d5d62939..db792fe3 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/NativeRebexFtpUploadHandler.cs @@ -20,6 +20,7 @@ public async Task Handle(XchangeFile xchangeFile) case "sftpssh": var sftpssh = new Sftp(); await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22); + FtpProtocol.VerifyHostKey(sftpssh, _options.HostKeyFingerprint); var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey)); var sshPrivateKey = new SshPrivateKey(keyBytes, _options.Password); @@ -31,6 +32,7 @@ public async Task Handle(XchangeFile xchangeFile) case "sftp": var sftp = new Sftp(); await sftp.ConnectAsync(_options.Host, _options.Port ?? 22); + FtpProtocol.VerifyHostKey(sftp, _options.HostKeyFingerprint); ftpOrSftp = sftp; await ftpOrSftp.LoginAsync(_options.Username, _options.Password); break; @@ -56,7 +58,9 @@ public async Task Handle(XchangeFile xchangeFile) await using var stream = new MemoryStream(bytes); - var filename = xchangeFile.Filename; + var filename = string.IsNullOrWhiteSpace(xchangeFile.Filename) + ? null + : FtpProtocol.SafeFileName(xchangeFile.Filename); if (string.IsNullOrWhiteSpace(filename)) { var currentDate = DateTime.UtcNow; diff --git a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/RebexFtpUploadHandlerInput.cs b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/RebexFtpUploadHandlerInput.cs index f9957985..212332f7 100644 --- a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/RebexFtpUploadHandlerInput.cs +++ b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/RebexFtpUploadHandlerInput.cs @@ -37,4 +37,7 @@ public class RebexFtpUploadHandlerInput [Secure] [Description("Private key used for SFTP authentication instead of a password.")] public string? PrivateKey { get; set; } + + [Description("SHA-256 fingerprint of the SFTP server's host key, as 'ssh-keygen -lf' prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent.")] + public string? HostKeyFingerprint { get; set; } } diff --git a/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/adapter.json b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/adapter.json new file mode 100644 index 00000000..b8f95aa7 --- /dev/null +++ b/SW.Bitween.NativeAdapters/RebexFtpUploadHandler/adapter.json @@ -0,0 +1,85 @@ +{ + "manifestVersion": 1, + "id": "nativerebexftpuploadhandler", + "displayName": "FTP / SFTP upload (Rebex)", + "summary": "Uploads the message as a file to an FTP or SFTP server. Uses the Rebex client and needs a Rebex license.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "ftp", + "sftp", + "file" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "FTP/SFTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, the protocol\u0027s default port is used." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "FTP/SFTP account username." + }, + { + "name": "Password", + "type": "text", + "secret": true, + "description": "Account password, or the private key passphrase when using SFTP with a private key." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to upload the file into." + }, + { + "name": "FileNamePrefix", + "type": "text", + "description": "Prefix added to the uploaded file\u0027s name." + }, + { + "name": "DataEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to write the file contents (e.g. utf8)." + }, + { + "name": "Protocol", + "type": "text", + "default": "sftp", + "description": "Transfer protocol to use (sftp or ftp)." + }, + { + "name": "PrivateKey", + "type": "text", + "secret": true, + "description": "Private key used for SFTP authentication instead of a password." + }, + { + "name": "HostKeyFingerprint", + "type": "text", + "description": "SHA-256 fingerprint of the SFTP server\u0027s host key, as \u0027ssh-keygen -lf\u0027 prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs b/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs index 31df8cd9..8080833c 100644 --- a/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs +++ b/SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs @@ -4,7 +4,7 @@ namespace SW.Bitween.NativeAdapters.RebexPop3Receiver; -public class NativeRebexPop3Receiver(string? licenseKey = null) : INativeInfolinkReceiver, IRequiresRebexLicense +public class NativeRebexPop3Receiver(string? licenseKey = null) : INativeInfolinkReceiver, IRequiresRebexLicense, IDisposable { private RebexPop3ReceiverInput _options = new(); private Pop3 _pop3 = new(); @@ -46,8 +46,10 @@ public async Task GetFile(string fileId) if (message.Attachments.Count < 1) return new XchangeFile(message.BodyText, message.Subject); + // The attachment is already in hand with the message. This used to also save the raw message + // to a local file named after the attachment - a name the sender chooses, so any mail to the + // mailbox could write wherever that name pointed - and nothing ever read the file. var attachment = message.Attachments[0]; - await _pop3.GetMessageAsync(sequenceNumber, attachment.FileName); await using var stream = attachment.GetContentStream(); using var memoryStream = new MemoryStream(); @@ -76,4 +78,13 @@ public void InitializeStartupValues(IDictionary settings) } public Type StartupValuesType => typeof(RebexPop3ReceiverInput); + + /// + /// Closes a connection a run left open - one that failed before Finalize. Without the QUIT that + /// Finalize sends, the server keeps the messages this run marked for deletion. + /// + public void Dispose() + { + try { _pop3.Dispose(); } catch { /* closing only */ } + } } diff --git a/SW.Bitween.NativeAdapters/RebexPop3Receiver/adapter.json b/SW.Bitween.NativeAdapters/RebexPop3Receiver/adapter.json new file mode 100644 index 00000000..3c8ead0e --- /dev/null +++ b/SW.Bitween.NativeAdapters/RebexPop3Receiver/adapter.json @@ -0,0 +1,59 @@ +{ + "manifestVersion": 1, + "id": "nativerebexpop3receiver", + "displayName": "Email pickup (POP3, Rebex)", + "summary": "Reads messages from a POP3 mailbox on a schedule, using the Rebex client.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "email", + "pop3" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "POP3 server hostname." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "POP3 account username." + }, + { + "name": "Password", + "type": "text", + "required": true, + "secret": true, + "description": "POP3 account password." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of messages to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode message contents (e.g. utf8)." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs b/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs index 01af398c..ad1557a7 100644 --- a/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs +++ b/SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs @@ -13,7 +13,7 @@ public class NativeS3Receiver : INativeInfolinkReceiver, IDisposable /// /// The clients, or an error that says what actually went wrong. Both fields are null until - /// Initialize() runs, because that is when the adapter's settings arrive — so they cannot be + /// Initialize() runs, because that is when the adapter's settings arrive - so they cannot be /// readonly, and every use would otherwise carry a bare `!` that asserts something no caller /// can see is true. /// diff --git a/SW.Bitween.NativeAdapters/S3Receiver/adapter.json b/SW.Bitween.NativeAdapters/S3Receiver/adapter.json new file mode 100644 index 00000000..737873ea --- /dev/null +++ b/SW.Bitween.NativeAdapters/S3Receiver/adapter.json @@ -0,0 +1,78 @@ +{ + "manifestVersion": 1, + "id": "natives3receiver", + "displayName": "Amazon S3 pickup", + "summary": "Picks up files from an S3-compatible bucket on a schedule.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "s3", + "aws", + "storage", + "file" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "AccessKeyId", + "type": "text", + "required": true, + "default": "", + "description": "S3-compatible access key ID." + }, + { + "name": "SecretAccessKey", + "type": "text", + "required": true, + "secret": true, + "description": "S3-compatible secret access key." + }, + { + "name": "ServiceUrl", + "type": "text", + "required": true, + "default": "", + "description": "S3-compatible service endpoint URL." + }, + { + "name": "BucketName", + "type": "text", + "required": true, + "default": "", + "description": "Name of the bucket to read files from." + }, + { + "name": "FolderName", + "type": "text", + "description": "Folder (object key prefix) within the bucket to read files from." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of files to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode file contents (e.g. utf8)." + }, + { + "name": "DeleteMovesFileTo", + "type": "text", + "description": "Folder to move a file to after it\u0027s received. If empty, the file is deleted instead." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/S3UploadHandler/NativeS3UploadHandler.cs b/SW.Bitween.NativeAdapters/S3UploadHandler/NativeS3UploadHandler.cs index 674d88bd..864a013a 100644 --- a/SW.Bitween.NativeAdapters/S3UploadHandler/NativeS3UploadHandler.cs +++ b/SW.Bitween.NativeAdapters/S3UploadHandler/NativeS3UploadHandler.cs @@ -9,7 +9,10 @@ public class NativeS3UploadHandler : INativeInfolinkHandler public async Task Handle(XchangeFile xchangeFile) { - using var cloudFiles = new CloudFilesService(new CloudFilesOptions + // S3CloudFilesOptions, not the base CloudFilesOptions: the S3 service casts what it is + // given to its own options type, so the base type failed every upload with an + // InvalidCastException before a byte was sent. + using var cloudFiles = new CloudFilesService(new S3CloudFilesOptions { AccessKeyId = _options.AccessKeyId, SecretAccessKey = _options.SecretAccessKey, diff --git a/SW.Bitween.NativeAdapters/S3UploadHandler/adapter.json b/SW.Bitween.NativeAdapters/S3UploadHandler/adapter.json new file mode 100644 index 00000000..943f099f --- /dev/null +++ b/SW.Bitween.NativeAdapters/S3UploadHandler/adapter.json @@ -0,0 +1,77 @@ +{ + "manifestVersion": 1, + "id": "natives3uploadhandler", + "displayName": "Amazon S3 upload", + "summary": "Writes the message as a file to an S3-compatible bucket.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "s3", + "aws", + "storage", + "file" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "AccessKeyId", + "type": "text", + "required": true, + "default": "", + "description": "S3-compatible access key ID." + }, + { + "name": "SecretAccessKey", + "type": "text", + "required": true, + "secret": true, + "description": "S3-compatible secret access key." + }, + { + "name": "ServiceUrl", + "type": "text", + "required": true, + "default": "", + "description": "S3-compatible service endpoint URL." + }, + { + "name": "BucketName", + "type": "text", + "required": true, + "default": "", + "description": "Name of the bucket to upload files to." + }, + { + "name": "FolderName", + "type": "text", + "description": "Folder (object key prefix) within the bucket to upload files to." + }, + { + "name": "FileName", + "type": "text", + "description": "Name to give the uploaded object. If empty, a name is generated from the current timestamp and a GUID." + }, + { + "name": "FileExtension", + "type": "text", + "description": "Extension appended to the generated object key (e.g. json, csv). Ignored if FileName is set." + }, + { + "name": "ContentType", + "type": "text", + "default": "text/plain", + "description": "Content-Type to set on the uploaded object (e.g. text/plain, application/json)." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj index a0804147..f197e0e3 100644 --- a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj +++ b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj @@ -24,8 +24,15 @@ + + + + + + + diff --git a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs index c37d4eab..841f3687 100644 --- a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs +++ b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs @@ -1,8 +1,11 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using SW.Bitween.NativeAdapters.AzureBlobReceiver; using SW.Bitween.NativeAdapters.AzureBlobUploadHandler; using SW.Bitween.NativeAdapters.HttpReceiver; using SW.Bitween.NativeAdapters.Pop3Receiver; +using SW.Bitween.NativeAdapters.FileTransfers; using SW.Bitween.NativeAdapters.RebexFtpReceiver; using SW.Bitween.NativeAdapters.RebexFtpUploadHandler; using SW.Bitween.NativeAdapters.RebexPop3Receiver; @@ -16,12 +19,28 @@ public static class ServiceCollectionExtensions { /// /// Reads the current Rebex license key. Called per adapter instance rather than once here, - /// because the key is a setting that can be changed while the app is running — pasting one in + /// because the key is a setting that can be changed while the app is running - pasting one in /// makes the Rebex adapters usable without a restart. /// + /// + /// Whether the HTTP adapters may reach private and loopback addresses; see + /// . Link-local addresses are refused either way. + /// + /// + /// When a destination's circuit opens and for how long; see . + /// when not given. + /// public static void AddNativeAdapters(this IServiceCollection serviceCollection, - Func? rebexLicenseKey = null) + Func? rebexLicenseKey = null, + Func? blockPrivateNetworks = null, + Func? circuitBreaker = null) { + // One per process, so every adapter instance (they are scoped) learns from the others' calls. + serviceCollection.TryAddSingleton(sp => new DestinationCircuits( + circuitBreaker?.Invoke(sp) ?? CircuitBreakerSettings.Default, + sp.GetService(), + sp.GetService>())); + serviceCollection.ConfigureHttpClientDefaults(builder => { builder.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler @@ -30,6 +49,15 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection, MaxConnectionsPerServer = 100 }); }); + // The adapters' own client, which carries the address guard. Named rather than set on the + // defaults above so the guard covers what adapters call and nothing else in the process. + serviceCollection.AddHttpClient(DynamicHttpProxy.ClientName) + .ConfigurePrimaryHttpMessageHandler(sp => new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + MaxConnectionsPerServer = 100, + ConnectCallback = OutboundAddressGuard.ConnectCallback(blockPrivateNetworks?.Invoke(sp) ?? false) + }); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); @@ -67,6 +95,17 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection, serviceCollection.AddScoped(); serviceCollection.AddScoped(); + // SFTP, and FTP/FTPS, with open-source clients: always offered, no license needed. + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + if (rebexLicenseKey is not null) { serviceCollection.AddScoped(sp => new NativeRebexPop3Receiver(rebexLicenseKey(sp))); diff --git a/SW.Bitween.NativeAdapters/SftpReceiver/adapter.json b/SW.Bitween.NativeAdapters/SftpReceiver/adapter.json new file mode 100644 index 00000000..72a6ed3f --- /dev/null +++ b/SW.Bitween.NativeAdapters/SftpReceiver/adapter.json @@ -0,0 +1,102 @@ +{ + "manifestVersion": 1, + "id": "nativesftpreceiver", + "displayName": "SFTP pickup", + "summary": "Picks up files from an SFTP server on a schedule, signing in with a password or a private key, and leaving files still being written for the next run.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "sftp", + "file" + ], + "categories": [ + "Intake" + ], + "kinds": [ + "receiver" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "SFTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, 22 is used." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "SFTP account username." + }, + { + "name": "Password", + "type": "text", + "secret": true, + "description": "Account password, or the private key's passphrase when a private key is set." + }, + { + "name": "PrivateKey", + "type": "text", + "secret": true, + "description": "Private key to sign in with instead of a password, in PEM or OpenSSH format. A PEM key pasted onto one line is re-wrapped." + }, + { + "name": "HostKeyFingerprint", + "type": "text", + "description": "SHA-256 fingerprint of the server's host key, as 'ssh-keygen -lf' prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent." + }, + { + "name": "TimeoutSeconds", + "type": "number", + "default": "60", + "description": "Seconds to wait for the server at each step - connecting, opening the SFTP session, each request - before the run fails. Default 60." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to read files from." + }, + { + "name": "BatchSize", + "type": "number", + "default": "50", + "description": "Maximum number of files to fetch per polling batch." + }, + { + "name": "ResponseEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to decode file contents: utf8, or base64 for binary files." + }, + { + "name": "DeleteMovesFileTo", + "type": "text", + "description": "Folder to move a file to after it's received. If empty, the file is deleted instead." + }, + { + "name": "CheckFileExistence", + "type": "boolean", + "default": "True", + "description": "Whether to verify the file still exists before attempting to delete or move it after receiving." + }, + { + "name": "MinimumFileAgeSeconds", + "type": "number", + "default": "0", + "description": "Seconds a file must have gone unchanged before it is taken, so one still being uploaded is left for the next run. 0 takes every file listed." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/SftpUploadHandler/adapter.json b/SW.Bitween.NativeAdapters/SftpUploadHandler/adapter.json new file mode 100644 index 00000000..5aab99d1 --- /dev/null +++ b/SW.Bitween.NativeAdapters/SftpUploadHandler/adapter.json @@ -0,0 +1,89 @@ +{ + "manifestVersion": 1, + "id": "nativesftpuploadhandler", + "displayName": "SFTP upload", + "summary": "Uploads the message as a file to an SFTP server, signing in with a password or a private key.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "sftp", + "file" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "SFTP server hostname." + }, + { + "name": "Port", + "type": "number", + "description": "Server port. If empty, 22 is used." + }, + { + "name": "Username", + "type": "text", + "required": true, + "default": "", + "description": "SFTP account username." + }, + { + "name": "Password", + "type": "text", + "secret": true, + "description": "Account password, or the private key's passphrase when a private key is set." + }, + { + "name": "PrivateKey", + "type": "text", + "secret": true, + "description": "Private key to sign in with instead of a password, in PEM or OpenSSH format. A PEM key pasted onto one line is re-wrapped." + }, + { + "name": "HostKeyFingerprint", + "type": "text", + "description": "SHA-256 fingerprint of the server's host key, as 'ssh-keygen -lf' prints it (SHA256:...). When set, a server presenting any other key is refused before credentials are sent." + }, + { + "name": "TimeoutSeconds", + "type": "number", + "default": "60", + "description": "Seconds to wait for the server at each step - connecting, opening the SFTP session, each request - before the run fails. Default 60." + }, + { + "name": "TargetPath", + "type": "text", + "description": "Remote directory to upload the file into." + }, + { + "name": "FileNamePrefix", + "type": "text", + "description": "Prefix added to the uploaded file's name." + }, + { + "name": "FileName", + "type": "text", + "description": "The uploaded file's name, in place of the message's own. For a JSON message it may use the message's fields, as order-{{ orderId }}.json, so each message gets its own file. Empty keeps the message's own name, or the time when it has none." + }, + { + "name": "DataEncoding", + "type": "text", + "default": "utf8", + "description": "Encoding used to write the file contents: utf8, or base64 to write the decoded bytes." + } + ] +} diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs index 6cc74e84..942c824b 100644 --- a/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs +++ b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs @@ -16,12 +16,14 @@ namespace SW.Bitween.NativeAdapters.SmtpHandler; /// Sends the payload as an email, with the subject and body written as templates over it. /// /// -/// Built for cases where the recipient is a person rather than a system — a retry budget running -/// out, a notifier on a failed exchange — which is why the subject and body are templated instead +/// Built for cases where the recipient is a person rather than a system - a retry budget running +/// out, a notifier on a failed exchange - which is why the subject and body are templated instead /// of the payload being emailed raw. A JSON blob in an inbox tells nobody anything. /// -public class NativeSmtpHandler : INativeInfolinkHandler +public class NativeSmtpHandler(DestinationCircuits? circuits = null) : INativeInfolinkHandler { + readonly DestinationCircuits circuits = circuits ?? DestinationCircuits.None; + private SmtpHandlerInput _options = new(); public string Name => "NativeSmtpHandler"; @@ -56,22 +58,47 @@ public async Task Handle(XchangeFile xchangeFile) // What the callback softens is the other outcome: the lookup needs the CA's OCSP or CRL server // to be reachable, which the corporate networks Bitween runs inside routinely block, and // MailKit's default treats "could not find out" exactly like "revoked". That rejected a - // perfectly good Gmail certificate — one OpenSSL accepts on the same machine — and the alert + // perfectly good Gmail certificate - one OpenSSL accepts on the same machine - and the alert // never went out. Soft-failing an undeterminable status is what browsers and mail clients do; // every other defect, revocation included, still fails. client.CheckCertificateRevocation = true; - client.ServerCertificateValidationCallback = (_, _, chain, errors) => - IsCertificateAcceptable(errors, - chain?.ChainStatus.Select(s => s.Status) ?? Enumerable.Empty()); + // A pinned certificate is trusted for exactly what it is, a self-signed one included: how a + // relay with its own certificate is used without trusting every certificate. + var pinned = string.IsNullOrWhiteSpace(_options.CertificateThumbprint) + ? null + : FtpTransfer.NormalizeThumbprint(_options.CertificateThumbprint); + client.ServerCertificateValidationCallback = (_, certificate, chain, errors) => + pinned != null + ? certificate != null && FtpTransfer.Thumbprint(certificate) == pinned + : IsCertificateAcceptable(errors, + chain?.ChainStatus.Select(s => s.Status) ?? Enumerable.Empty()); // Named rather than left to Auto: on any port but 465, Auto means "encrypt if the server - // offers it", so a server that does not offer STARTTLS — or an offer stripped in transit — + // offers it", so a server that does not offer STARTTLS - or an offer stripped in transit - // silently continues in the clear. StartTls demands it and fails if it is not there. 465 is // the implicit-TLS port, where the handshake happens before any of that is negotiable. var security = _options.UseTls ? _options.Port == 465 ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTls : SecureSocketOptions.None; + // Admitted once the message is built: one that can't be built fails the same whether the relay is up or not. + using var pass = circuits.Enter($"smtp://{_options.Host}:{_options.Port}"); + try + { + await Send(client, security, message); + } + catch (Exception e) + { + pass.Failed(e); + throw; + } + pass.Succeeded(); + + return new XchangeFile(subject, xchangeFile.Filename); + } + + private async Task Send(SmtpClient client, SecureSocketOptions security, MimeMessage message) + { await client.ConnectAsync(_options.Host, _options.Port, security); // A relay that accepts unauthenticated mail from inside the network is a normal setup, so @@ -91,8 +118,6 @@ await client.AuthenticateAsync( await client.SendAsync(message); await client.DisconnectAsync(true); - - return new XchangeFile(subject, xchangeFile.Filename); } private static void AddAddresses(InternetAddressList list, string? addresses) @@ -108,16 +133,11 @@ private static void AddAddresses(InternetAddressList list, string? addresses) /// Renders a template against the payload, or returns it unchanged when the payload is not JSON. /// /// - /// Non-JSON payloads are normal for a pipeline handler — a CSV or a flat file on its way out — + /// Non-JSON payloads are normal for a pipeline handler - a CSV or a flat file on its way out - /// and those have no fields to substitute. A broken template still throws, so a typo in a /// placeholder is not quietly emailed as literal text. /// - internal static string Fill(string template, string payload) - { - if (string.IsNullOrEmpty(template) || !LooksLikeJson(payload)) return template; - - return ScribanJsonHelper.RenderText(template, payload); - } + internal static string Fill(string template, string payload) => ScribanJsonHelper.FillIfJson(template, payload); /// /// Whether a server certificate should be accepted, given what validation found wrong with it. @@ -125,7 +145,7 @@ internal static string Fill(string template, string payload) /// /// Only one defect is tolerated: a revocation status that could not be established, because the /// CA's OCSP or CRL server was unreachable. A certificate the CA has revoked, an untrusted root, - /// a wrong hostname and an expired certificate are all still refused — as is any chain flag not + /// a wrong hostname and an expired certificate are all still refused - as is any chain flag not /// named here, so a defect nobody thought about fails closed rather than slipping through. /// internal static bool IsCertificateAcceptable(SslPolicyErrors errors, @@ -144,22 +164,4 @@ internal static bool IsCertificateAcceptable(SslPolicyErrors errors, // alongside "could not check" has to fail. return chainStatus.All(status => (status & ~undeterminable) == X509ChainStatusFlags.NoError); } - - private static bool LooksLikeJson(string payload) - { - if (string.IsNullOrWhiteSpace(payload)) return false; - - var trimmed = payload.TrimStart(); - if (trimmed[0] is not ('{' or '[')) return false; - - try - { - JToken.Parse(payload); - return true; - } - catch - { - return false; - } - } } diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs index b190827b..fd2145c5 100644 --- a/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs +++ b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs @@ -24,6 +24,9 @@ public class SmtpHandlerInput [Description("Encrypt the connection, choosing STARTTLS or SSL to match the port. Turn off only for an internal relay with no TLS.")] public bool UseTls { get; set; } = true; + [Description("The SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before anything is sent. When empty, the certificate must be valid.")] + public string? CertificateThumbprint { get; set; } + [Required] [Description("Address the message is sent from.")] public string From { get; set; } = string.Empty; diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/adapter.json b/SW.Bitween.NativeAdapters/SmtpHandler/adapter.json new file mode 100644 index 00000000..45499364 --- /dev/null +++ b/SW.Bitween.NativeAdapters/SmtpHandler/adapter.json @@ -0,0 +1,111 @@ +{ + "manifestVersion": 1, + "id": "nativesmtphandler", + "displayName": "Email (SMTP)", + "summary": "Sends the message as an email, with the subject and body written as templates over it.", + "publisher": { + "name": "Simplify9", + "url": "https://simplify9.com" + }, + "license": "Proprietary", + "tags": [ + "email", + "smtp", + "notification" + ], + "categories": [ + "Delivery" + ], + "kinds": [ + "handler" + ], + "runtime": "native", + "language": "csharp", + "lifecycle": "classic", + "properties": [ + { + "name": "Host", + "type": "text", + "required": true, + "default": "", + "description": "SMTP server hostname." + }, + { + "name": "Port", + "type": "number", + "default": "587", + "description": "SMTP server port. 587 for STARTTLS, 465 for implicit SSL, 25 for an unencrypted relay." + }, + { + "name": "Username", + "type": "text", + "description": "SMTP username. Leave empty to authenticate as the From address, or for a relay that needs no credentials." + }, + { + "name": "Password", + "type": "text", + "secret": true, + "description": "SMTP password. Leave empty for a relay that needs no credentials." + }, + { + "name": "UseTls", + "type": "boolean", + "default": "True", + "description": "Encrypt the connection, choosing STARTTLS or SSL to match the port. Turn off only for an internal relay with no TLS." + }, + { + "name": "CertificateThumbprint", + "type": "text", + "description": "The SHA-256 thumbprint of the server's certificate, in hex. When set, exactly that certificate is trusted, a self-signed one included, and any other is refused before anything is sent. When empty, the certificate must be valid." + }, + { + "name": "From", + "type": "text", + "required": true, + "default": "", + "description": "Address the message is sent from." + }, + { + "name": "FromName", + "type": "text", + "description": "Display name shown beside the From address, e.g. Bitween Alerts." + }, + { + "name": "To", + "type": "text", + "required": true, + "default": "", + "description": "Recipients, separated by commas." + }, + { + "name": "Cc", + "type": "text", + "description": "Carbon-copy recipients, separated by commas." + }, + { + "name": "Bcc", + "type": "text", + "description": "Blind carbon-copy recipients, separated by commas." + }, + { + "name": "Subject", + "type": "text", + "required": true, + "default": "", + "description": "Subject line. Placeholders in the incoming payload are substituted, e.g. 'Retries stopped for {{ SubscriptionName }}'." + }, + { + "name": "Body", + "type": "text", + "required": true, + "default": "", + "description": "Message body. Uses the same template syntax as the JSON mapper, so payload fields can be referenced directly, e.g. '{{ GroupName }} used all {{ MaxAttemptsTotal }} retries.'" + }, + { + "name": "IsHtml", + "type": "boolean", + "default": "True", + "description": "Send the body as HTML. Turn off to send it as plain text." + } + ] +} diff --git a/SW.Bitween.PgSql/BitweenDbContext.cs b/SW.Bitween.PgSql/BitweenDbContext.cs index 2cdd9220..18000d7d 100644 --- a/SW.Bitween.PgSql/BitweenDbContext.cs +++ b/SW.Bitween.PgSql/BitweenDbContext.cs @@ -78,7 +78,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) //b.ToTable("Partners"); b.Metadata.SetNavigationAccessMode(PropertyAccessMode.Field); b.Property(p => p.Name).IsRequired().HasMaxLength(200); - b.Property(p => p.AdapterProperties).HasColumnType("jsonb"); + b.Property(p => p.AdapterProperties).EncryptedJsonb(); b.Property(p => p.SecretProperties).HasColumnType("jsonb"); b.Property(p => p.LoginIdentity).HasMaxLength(500); b.HasIndex(p => p.LoginIdentity).IsUnique(); @@ -89,6 +89,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) apicred.ToTable("partner_api_credential"); apicred.Property(p => p.Name).IsRequired().HasMaxLength(500); apicred.Property(p => p.Key).IsRequired().HasMaxLength(500); + apicred.Property(p => p.KeyPrefix).HasMaxLength(10); apicred.HasIndex(p => p.Key).IsUnique(); apicred.WithOwner().HasForeignKey("PartnerId"); @@ -138,7 +139,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsRequired().OnDelete(DeleteBehavior.Restrict); }); - // NOTE: this context does NOT call base.OnModelCreating — it redeclares the model. + // NOTE: this context does NOT call base.OnModelCreating - it redeclares the model. // Anything configured only in SW.Bitween.Api's context is inert here. DataSource // reached the model anyway, by convention, through the BusGateway.DataSource // navigation; InboundMessage has no such navigation and has to be declared. @@ -150,6 +151,74 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) cl.Property(i => i.OwnerNode).HasMaxLength(200); }); + modelBuilder.Entity(b => + { + b.ToTable("operator_action"); + b.HasKey(a => a.Id); + b.Property(a => a.Id).ValueGeneratedOnAdd(); + b.Property(a => a.Action).HasMaxLength(50).IsRequired(); + b.Property(a => a.Target).HasMaxLength(500); + b.Property(a => a.Detail).HasMaxLength(2000); + b.Property(a => a.AccountId).HasMaxLength(50); + b.HasIndex(a => a.OccurredOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("api_key_use"); + b.HasKey(u => new { u.PartnerId, u.KeyName }); + b.Property(u => u.KeyName).HasMaxLength(500); + }); + + modelBuilder.Entity(b => + { + b.ToTable("cluster_node"); + b.HasKey(i => i.Id); + b.Property(i => i.Id).HasMaxLength(200); + b.Property(i => i.Host).HasMaxLength(200); + b.Property(i => i.Version).HasMaxLength(50); + b.Property(i => i.Runtimes).HasMaxLength(100); + b.Property(i => i.ResidentHealth).HasMaxLength(4000); + b.HasIndex(i => i.LastSeenOn); + }); + + // Metrics: hourly rollups each node adds to once a minute, and broker snapshots. Keys lead + // with the hour, so reading a time range and pruning old hours both use the key itself. + modelBuilder.Entity(b => + { + b.ToTable("exchange_stats"); + b.HasKey(p => new { p.Hour, p.Node, p.SubscriptionId, p.PartnerId }); + b.Property(p => p.Node).HasMaxLength(100); + }); + + modelBuilder.Entity(b => + { + b.ToTable("adapter_stats"); + b.HasKey(p => new { p.Hour, p.Node, p.AdapterId, p.Version, p.Role, p.Runtime }); + b.Property(p => p.Node).HasMaxLength(100); + b.Property(p => p.AdapterId).HasMaxLength(200); + b.Property(p => p.Version).HasMaxLength(50); + b.Property(p => p.Role).HasMaxLength(20); + b.Property(p => p.Runtime).HasMaxLength(20); + }); + + modelBuilder.Entity(b => + { + b.ToTable("error_stats"); + b.HasKey(p => new { p.Hour, p.Node, p.Scope, p.ScopeKey, p.Fingerprint }); + b.Property(p => p.Node).HasMaxLength(100); + b.Property(p => p.Scope).HasMaxLength(20); + b.Property(p => p.ScopeKey).HasMaxLength(200); + b.Property(p => p.Fingerprint).HasMaxLength(16); + b.Property(p => p.Sample).HasMaxLength(500); + }); + + modelBuilder.Entity(b => + { + b.ToTable("queue_snapshot"); + b.HasKey(p => p.At); + }); + modelBuilder.Entity(st => { st.ToTable("data_source_statement"); @@ -159,7 +228,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) st.Property(p => p.Sql).IsRequired(); st.Property(p => p.Description).HasMaxLength(1000); - // Column names, so the database's own identifier limit is the ceiling — 128 is + // Column names, so the database's own identifier limit is the ceiling - 128 is // above every engine's (Oracle allows 128, PostgreSQL 63). st.Property(p => p.CursorColumn).HasMaxLength(128); st.Property(p => p.KeyColumn).HasMaxLength(128); @@ -196,7 +265,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) im.ToTable("inbound_message"); // The dedupe key IS the primary key. Deduplication is decided by an insert - // failing, not by a lookup succeeding — see the type's remarks. + // failing, not by a lookup succeeding - see the type's remarks. im.HasKey(i => i.Id); im.Property(i => i.Id).HasMaxLength(400); im.Property(i => i.XchangeId).HasMaxLength(50); @@ -241,7 +310,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { gav.ToTable("global_adapter_values_set"); gav.HasKey(i => i.Id); - gav.Property(p => p.Values).HasColumnType("jsonb"); + gav.Property(p => p.Values).EncryptedJsonb(); gav.Property(p => p.SecretProperties).HasColumnType("jsonb"); }); modelBuilder.Entity(b => @@ -257,16 +326,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) }); - b.Property(p => p.HandlerProperties).HasColumnType("jsonb"); - b.Property(p => p.MapperProperties).HasColumnType("jsonb"); - b.Property(p => p.ReceiverProperties).HasColumnType("jsonb"); - b.Property(p => p.ValidatorProperties).HasColumnType("jsonb"); + b.Property(p => p.HandlerProperties).EncryptedJsonb(); + b.Property(p => p.MapperProperties).EncryptedJsonb(); + b.Property(p => p.ReceiverProperties).EncryptedJsonb(); + b.Property(p => p.ValidatorProperties).EncryptedJsonb(); b.Property(p => p.DocumentFilter).HasColumnType("jsonb"); b.Property(p => p.ResponseMessageTypeName).HasMaxLength(500); b.Property(p => p.MapperId).HasMaxLength(200); b.Property(p => p.HandlerId).HasMaxLength(200); + b.Property(p => p.ReceiverVersion).HasMaxLength(50); + b.Property(p => p.ValidatorVersion).HasMaxLength(50); + b.Property(p => p.MapperVersion).HasMaxLength(50); + b.Property(p => p.HandlerVersion).HasMaxLength(50); b.Property(p => p.ReceiverId).HasMaxLength(200); b.Property(p => p.ValidatorId).HasMaxLength(200); @@ -301,8 +374,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.InputName).HasMaxLength(200); b.Property(p => p.MapperId).HasMaxLength(200); b.Property(p => p.HandlerId).HasMaxLength(200); - b.Property(p => p.HandlerProperties).HasColumnType("jsonb"); - b.Property(p => p.MapperProperties).HasColumnType("jsonb"); + b.Property(p => p.HandlerProperties).EncryptedJsonb(); + b.Property(p => p.MapperProperties).EncryptedJsonb(); b.Property(p => p.SourceValues).HasColumnType("jsonb"); b.Property(p => p.SourceXchangeId).HasMaxLength(50); b.Property(p => p.InputContentType).HasMaxLength(200); @@ -338,6 +411,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.RetryGroupId); b.Property(p => p.AttemptNumber); b.HasIndex(p => p.RetryGroupId); + b.Property(p => p.NodeName).HasMaxLength(100); + b.Property(p => p.MapperVersion).HasMaxLength(50); + b.Property(p => p.HandlerVersion).HasMaxLength(50); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); }); @@ -372,17 +448,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // No b-tree on PropertiesRaw. The only query on it is the Exchanges page's substring // search, a leading-wildcard LIKE no b-tree can serve (Postgres has a trigram index // for it, PromotedPropertiesTrigramIndex). Having one made long values fail the - // save — after the delivery, so the retry delivered again — and it is what had + // save - after the delivery, so the retry delivered again - and it is what had // capped the column at 450 characters on SQL Server and 255 on MySQL. b.HasOne().WithOne().HasForeignKey(p => p.Id) .OnDelete(DeleteBehavior.Cascade); }); + // hstore by convention, so encrypted value by value rather than as one JSON document. + modelBuilder.Entity(b => + b.Property(p => p.Properties).EncryptedValues().HasColumnType("hstore")); + modelBuilder.Entity(b => { b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).HasMaxLength(100).IsRequired(); - b.Property(p => p.HandlerProperties).StoreAsJson(); + b.Property(p => p.HandlerProperties).StoreAsJson().Encrypted(); b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); }); @@ -392,6 +472,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.XchangeId).IsUnicode(false).HasMaxLength(50); // Retention deletes an exchange's notifications by exchange id. b.HasIndex(p => p.XchangeId); + // The quiet window looks up a notifier's recent sends. + b.HasIndex(p => new { p.NotifierId, p.FinishedOn }); }); modelBuilder.Entity(b => @@ -407,6 +489,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.EmailProvider).HasConversion(); b.Property(p => p.LoginMethods).HasConversion(); + // Added with service accounts; every account before them is a person (0). + b.Property(p => p.Kind).HasConversion(); b.HasData( new @@ -421,6 +505,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Disabled = false, Password = defaultPasswordHash, Role = AccountRole.Admin, + Kind = AccountKind.Person, FailedLoginCount = 0, // True for a fresh installation, whose password is the published default. // Installations that already exist are handled by the migration, which @@ -440,6 +525,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.LoginMethod).HasConversion(); }); + modelBuilder.Entity(b => + { + b.ToTable("ServiceTokens"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasOne().WithMany().HasForeignKey(p => p.AccountId).OnDelete(DeleteBehavior.Cascade); + + b.Property(p => p.Name).IsRequired().HasMaxLength(100); + b.Property(p => p.Hash).IsRequired().IsUnicode(false).HasMaxLength(64); + b.Property(p => p.Shown).IsRequired().IsUnicode(false).HasMaxLength(20); + b.Property(p => p.RevokedBy).HasMaxLength(200); + b.Property(p => p.LastUsedFrom).IsUnicode(false).HasMaxLength(64); + // Every sign-in looks a token up by its hash. + b.HasIndex(p => p.Hash).IsUnique(); + }); + modelBuilder.Entity(b => { b.ToTable("Roles"); @@ -471,7 +572,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Id).IsUnicode(false).HasMaxLength(200); }); - // ——— Audit trail ——— + // --- Audit trail --- // A table that only ever grows and is only ever appended to. The indexes answer the two // questions asked of it: the history of one row, and everything one save changed. modelBuilder.Entity(b => @@ -497,7 +598,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).IsRequired().HasMaxLength(200); b.Property(p => p.AlertHandlerId).HasMaxLength(200); - b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + b.Property(p => p.AlertHandlerProperties).StoreAsJson().Encrypted(); b.Property(p => p.Groups).HasConversion( groups => JsonSerializer.Serialize(groups, _polymorphicOpts), json => JsonSerializer.Deserialize>(json, _polymorphicOpts)!, @@ -517,6 +618,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) cp => cp == null ? null : JsonSerializer.Serialize(cp, _polymorphicOpts), json => json == null ? null : JsonSerializer.Deserialize(json, _polymorphicOpts) ); + b.Property(s => s.ExpectTraffic).HasConversion( + e => e == null ? null : JsonSerializer.Serialize(e, _polymorphicOpts), + json => json == null ? null : JsonSerializer.Deserialize(json, _polymorphicOpts) + ); }); modelBuilder.Entity(b => @@ -526,6 +631,60 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(p => p.On); }); + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.Scope).IsRequired().HasMaxLength(200); + b.Property(p => p.Key).IsRequired().HasMaxLength(200); + b.Property(p => p.XchangeId).IsRequired().IsUnicode(false).HasMaxLength(50); + b.HasIndex(p => new { p.Scope, p.Key }).IsUnique(); + b.HasIndex(p => p.CreatedOn); + }); + + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.MessageType).IsRequired().HasMaxLength(500); + b.Property(p => p.Body).IsRequired(); + b.Property(p => p.Headers).StoreAsJson(); + b.Property(p => p.LastError).HasMaxLength(2000); + b.HasIndex(p => p.PublishedOn); + }); + + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Language).HasMaxLength(20).IsUnicode(false).IsRequired(); + b.Property(p => p.Kind).HasMaxLength(20).IsUnicode(false); + b.Property(p => p.BaseVersion).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.FilesJson).IsRequired(); + b.Property(p => p.FilesHash).HasMaxLength(64).IsUnicode(false).IsRequired(); + b.Property(p => p.CreatedBy).HasMaxLength(200); + b.Property(p => p.ModifiedBy).HasMaxLength(200); + b.HasIndex(p => p.AdapterId); + }); + + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false).IsRequired(); + b.Property(p => p.Action).HasMaxLength(20).IsUnicode(false).IsRequired(); + b.Property(p => p.AccountId).HasMaxLength(100).IsUnicode(false); + b.HasIndex(p => new { p.AdapterId, p.OccurredOn }); + }); + + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.AdapterId).HasMaxLength(200).IsUnicode(false).IsRequired(); + b.Property(p => p.Version).HasMaxLength(50).IsUnicode(false); + b.Property(p => p.Path).HasMaxLength(500); + b.Property(p => p.AccountId).HasMaxLength(100).IsUnicode(false); + b.HasIndex(p => new { p.AdapterId, p.OccurredOn }); + }); + modelBuilder.Entity(b => { b.Property(p => p.Id).ValueGeneratedOnAdd(); @@ -545,7 +704,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasKey(p => new { p.SubscriptionId, p.GroupId }); b.Property(p => p.AlertMode).HasConversion(); b.Property(p => p.AlertHandlerId).HasMaxLength(200); - b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + b.Property(p => p.AlertHandlerProperties).StoreAsJson().Encrypted(); }); modelBuilder.UseSchedulerPostgreSql(Schema); diff --git a/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs index 7c5b10da..b5511139 100644 --- a/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs +++ b/SW.Bitween.PgSql/Migrations/20260719141125_AddDocumentCodeAndAutoId.cs @@ -37,7 +37,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "code", value: null); - // Code is optional — existing rows stay code = NULL rather than being + // Code is optional - existing rows stay code = NULL rather than being // backfilled from name, which risked duplicate-derived codes colliding // against the unique index on environments with less controlled data. migrationBuilder.CreateIndex( diff --git a/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs index dd4ea6fa..80c25550 100644 --- a/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs +++ b/SW.Bitween.PgSql/Migrations/20260726105636_AddRolesAndPermissions.cs @@ -88,7 +88,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // Every existing account keeps exactly the access it had: its coarse AccountRole // (Admin=0, Viewer=10, Member=20) becomes the matching built-in role. Anything - // unrecognised lands on Viewer — least privilege. Idempotent, so re-running or + // unrecognised lands on Viewer - least privilege. Idempotent, so re-running or // ordering against the seeded admin account can't produce duplicates. migrationBuilder.Sql(""" INSERT INTO infolink."AccountRoles" (account_id, role_id) diff --git a/SW.Bitween.PgSql/Migrations/20260901121718_PromotedPropertiesTrigramIndex.cs b/SW.Bitween.PgSql/Migrations/20260901121718_PromotedPropertiesTrigramIndex.cs index 2292acf2..62798967 100644 --- a/SW.Bitween.PgSql/Migrations/20260901121718_PromotedPropertiesTrigramIndex.cs +++ b/SW.Bitween.PgSql/Migrations/20260901121718_PromotedPropertiesTrigramIndex.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.PgSql.Migrations /// LOWER(properties_raw) LIKE '%term%'. A leading wildcard has no fixed prefix to look /// up, so the b-tree already on that column can never serve it and every search reads the whole /// table. Measured on 1,000,000 exchanges: searching a promoted key took 6.2s end to end, of - /// which 4.8s was the rows query alone. With this index the same search is 228ms — the rows + /// which 4.8s was the rows query alone. With this index the same search is 228ms - the rows /// query drops to 4ms and what remains is the footer's count. /// /// A trigram (pg_trgm) GIN index indexes three-character sequences, which is what lets a @@ -21,8 +21,8 @@ namespace SW.Bitween.PgSql.Migrations /// /// Raw SQL rather than HasIndex because the index is on an expression, /// lower(properties_raw), and EF can only model indexes over plain columns. The - /// model-declarable alternative — a trigram index on the bare column, matched with - /// ILIKE — was measured and does work (98ms, index scan), but it needs + /// model-declarable alternative - a trigram index on the bare column, matched with + /// ILIKE - was measured and does work (98ms, index scan), but it needs /// EF.Functions.ILike at the call site, and Xchanges/Search.cs lives in /// SW.Bitween.Api, which deliberately references only EntityFrameworkCore.Relational and no /// provider. Keeping the Postgres-specific part inside a Postgres-only migration is the lesser @@ -30,7 +30,7 @@ namespace SW.Bitween.PgSql.Migrations /// /// Built CONCURRENTLY: a plain CREATE INDEX holds an ACCESS EXCLUSIVE lock for the whole build /// (~6s per million rows locally, longer on a managed instance), and every exchange the engine - /// processes writes a row to this table — so a plain build would stall processing for the + /// processes writes a row to this table - so a plain build would stall processing for the /// duration. CONCURRENTLY cannot run inside a transaction, hence suppressTransaction. /// public partial class PromotedPropertiesTrigramIndex : Migration @@ -42,7 +42,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // A CONCURRENTLY build that fails part-way leaves the index behind marked INVALID // rather than removing it. Postgres ignores an invalid index when planning, but it - // still occupies the name — so the IF NOT EXISTS below would skip the rebuild, the + // still occupies the name - so the IF NOT EXISTS below would skip the rebuild, the // migration would be recorded as applied, and the search would quietly keep scanning // the whole table while the deploy looked green. Clear that state first. // @@ -75,7 +75,7 @@ ON infolink.xchange_promoted_properties // runs, so until it does, the planner has no idea how selective // lower(properties_raw) LIKE ... is and can pick a far worse plan than it would with no // index at all. Measured on the million-row copy: 257ms with statistics, 19,437ms - // without — slower than the 8,103ms it takes with no index whatsoever. Autovacuum would + // without - slower than the 8,103ms it takes with no index whatsoever. Autovacuum would // eventually analyze the table on its own; this stops the window between deploying and // that happening from being a regression. migrationBuilder.Sql("ANALYZE infolink.xchange_promoted_properties;"); diff --git a/SW.Bitween.PgSql/Migrations/20260907082337_MergeExternalBusWithAuditTrail.cs b/SW.Bitween.PgSql/Migrations/20260907082337_MergeExternalBusWithAuditTrail.cs index d4ae4a15..929c9092 100644 --- a/SW.Bitween.PgSql/Migrations/20260907082337_MergeExternalBusWithAuditTrail.cs +++ b/SW.Bitween.PgSql/Migrations/20260907082337_MergeExternalBusWithAuditTrail.cs @@ -13,14 +13,14 @@ public partial class MergeExternalBusWithAuditTrail : Migration /// /// Deliberately empty. /// - /// Two branches added migrations at the same time — the audit trail on releases/r10.0, the - /// external bus data sources here — so each side's model snapshot described only its own + /// Two branches added migrations at the same time - the audit trail on releases/r10.0, the + /// external bus data sources here - so each side's model snapshot described only its own /// half. A snapshot is generated from the model, and hand-merging generated code is how one /// silently stops matching it, so the merge took r10's snapshot wholesale and let EF /// regenerate from the combined model. /// /// Regenerating produces this migration, whose Up() would create the data sources, the - /// deduplication table and the cluster leases — all of which migrations already on this + /// deduplication table and the cluster leases - all of which migrations already on this /// branch create. Running it would fail on a fresh database and do nothing on an existing /// one. What is worth keeping is the snapshot beside it, which now describes both halves. /// MigrationDriftTests is what proves that, and it is the reason this is safe to leave diff --git a/SW.Bitween.PgSql/Migrations/20260915090259_AddMustChangePassword.cs b/SW.Bitween.PgSql/Migrations/20260915090259_AddMustChangePassword.cs index bf2ae303..502f6201 100644 --- a/SW.Bitween.PgSql/Migrations/20260915090259_AddMustChangePassword.cs +++ b/SW.Bitween.PgSql/Migrations/20260915090259_AddMustChangePassword.cs @@ -23,7 +23,7 @@ protected override void Up(MigrationBuilder migrationBuilder) // EF scaffolded a plain UpdateData here, because as far as the model is concerned the // seeded row simply gained a column value. That would have demanded a new password from // every installation on the next deploy, including the ones that chose theirs years ago - // — a forced interruption for people who had already done the right thing. + // - a forced interruption for people who had already done the right thing. // // Matching on the stored hash narrows it to the installations the flag is actually for: // those still signing in with the password published in our public repository. On diff --git a/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.Designer.cs new file mode 100644 index 00000000..2a4ab913 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.Designer.cs @@ -0,0 +1,2832 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008091051_AddSubscriptionRunningSince")] + partial class AddSubscriptionRunningSince + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.cs b/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.cs new file mode 100644 index 00000000..e65ce294 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008091051_AddSubscriptionRunningSince.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddSubscriptionRunningSince : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "running_since", + schema: "infolink", + table: "subscription", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "running_since", + schema: "infolink", + table: "subscription"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.Designer.cs new file mode 100644 index 00000000..f40a2085 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.Designer.cs @@ -0,0 +1,2886 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008094911_AddOutboxMessages")] + partial class AddOutboxMessages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.cs b/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.cs new file mode 100644 index 00000000..7cf15702 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008094911_AddOutboxMessages.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddOutboxMessages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "outbox_message", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + message_type = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + body = table.Column(type: "text", nullable: false), + headers = table.Column(type: "text", nullable: true), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + published_on = table.Column(type: "timestamp with time zone", nullable: true), + claimed_until = table.Column(type: "timestamp with time zone", nullable: true), + attempts = table.Column(type: "integer", nullable: false), + last_error = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_outbox_message", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_outbox_message_published_on", + schema: "infolink", + table: "outbox_message", + column: "published_on"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "outbox_message", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.Designer.cs new file mode 100644 index 00000000..f79eca91 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.Designer.cs @@ -0,0 +1,2891 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008102236_AddPartnerKeyPrefix")] + partial class AddPartnerKeyPrefix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.cs b/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.cs new file mode 100644 index 00000000..537fe204 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008102236_AddPartnerKeyPrefix.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddPartnerKeyPrefix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "key_prefix", + schema: "infolink", + table: "partner_api_credential", + type: "character varying(10)", + maxLength: 10, + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "partner_api_credential", + keyColumns: new[] { "id", "partner_id" }, + keyValues: new object[] { 1, 1 }, + column: "key_prefix", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "key_prefix", + schema: "infolink", + table: "partner_api_credential"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.Designer.cs new file mode 100644 index 00000000..610e007f --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.Designer.cs @@ -0,0 +1,2891 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008102822_EncryptSecretColumns")] + partial class EncryptSecretColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.cs b/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.cs new file mode 100644 index 00000000..87e28423 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008102822_EncryptSecretColumns.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class EncryptSecretColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Deliberately empty. The credential columns gained an encrypting converter, which + // changes how EF compares the SYSTEM partner's seed row, so it proposed setting that + // row's adapter_properties to null. The schema is unchanged, and running that update + // would wipe any properties a deployment has set on the SYSTEM partner. This migration + // exists only so the snapshot records the converted model. + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.Designer.cs new file mode 100644 index 00000000..3eff72fe --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.Designer.cs @@ -0,0 +1,2895 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008114130_AddRefreshTokenSupersededOn")] + partial class AddRefreshTokenSupersededOn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.cs b/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.cs new file mode 100644 index 00000000..2b6b6389 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008114130_AddRefreshTokenSupersededOn.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddRefreshTokenSupersededOn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "superseded_on", + schema: "infolink", + table: "RefreshTokens", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "superseded_on", + schema: "infolink", + table: "RefreshTokens"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.Designer.cs new file mode 100644 index 00000000..b2d72791 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.Designer.cs @@ -0,0 +1,2899 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008122720_AddAccountMicrosoftIdentity")] + partial class AddAccountMicrosoftIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.cs b/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.cs new file mode 100644 index 00000000..73fce5d7 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008122720_AddAccountMicrosoftIdentity.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddAccountMicrosoftIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "microsoft_identity", + schema: "infolink", + table: "Accounts", + type: "text", + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + column: "microsoft_identity", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "microsoft_identity", + schema: "infolink", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.Designer.cs new file mode 100644 index 00000000..400d6798 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.Designer.cs @@ -0,0 +1,2910 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008134930_RetryRunFailuresAndQuietNotifications")] + partial class RetryRunFailuresAndQuietNotifications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.cs b/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.cs new file mode 100644 index 00000000..ac99e24e --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008134930_RetryRunFailuresAndQuietNotifications.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class RetryRunFailuresAndQuietNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "suppressed", + schema: "infolink", + table: "xchange_notification", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "run_failures", + schema: "infolink", + table: "delayed_retry", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "ix_xchange_notification_notifier_id_finished_on", + schema: "infolink", + table: "xchange_notification", + columns: new[] { "notifier_id", "finished_on" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_xchange_notification_notifier_id_finished_on", + schema: "infolink", + table: "xchange_notification"); + + migrationBuilder.DropColumn( + name: "suppressed", + schema: "infolink", + table: "xchange_notification"); + + migrationBuilder.DropColumn( + name: "run_failures", + schema: "infolink", + table: "delayed_retry"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.Designer.cs new file mode 100644 index 00000000..0118b12d --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.Designer.cs @@ -0,0 +1,2955 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008141134_AddIdempotencyKeys")] + partial class AddIdempotencyKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.cs b/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.cs new file mode 100644 index 00000000..f9124b8b --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008141134_AddIdempotencyKeys.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddIdempotencyKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "idempotency_key", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + scope = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + xchange_id = table.Column(type: "character varying(50)", unicode: false, maxLength: 50, nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_idempotency_key", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_idempotency_key_created_on", + schema: "infolink", + table: "idempotency_key", + column: "created_on"); + + migrationBuilder.CreateIndex( + name: "ix_idempotency_key_scope_key", + schema: "infolink", + table: "idempotency_key", + columns: new[] { "scope", "key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "idempotency_key", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.Designer.cs new file mode 100644 index 00000000..c965b1bb --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.Designer.cs @@ -0,0 +1,2963 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008142220_AddSubscriptionAutoPause")] + partial class AddSubscriptionAutoPause + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.cs b/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.cs new file mode 100644 index 00000000..702aebe6 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008142220_AddSubscriptionAutoPause.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddSubscriptionAutoPause : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "auto_pause_after_failures", + schema: "infolink", + table: "subscription", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "paused_automatically", + schema: "infolink", + table: "subscription", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "auto_pause_after_failures", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "paused_automatically", + schema: "infolink", + table: "subscription"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.Designer.cs new file mode 100644 index 00000000..5efb9cb8 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.Designer.cs @@ -0,0 +1,2967 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008143153_AddDocumentValidationSchema")] + partial class AddDocumentValidationSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.cs b/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.cs new file mode 100644 index 00000000..67936f5e --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008143153_AddDocumentValidationSchema.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddDocumentValidationSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "validation_schema", + schema: "infolink", + table: "document", + type: "text", + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "document", + keyColumn: "id", + keyValue: 10001, + column: "validation_schema", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "validation_schema", + schema: "infolink", + table: "document"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.Designer.cs b/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.Designer.cs new file mode 100644 index 00000000..efdb7a2f --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.Designer.cs @@ -0,0 +1,2987 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261008170022_AddSubscriptionAdapterVersions")] + partial class AddSubscriptionAdapterVersions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.cs b/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.cs new file mode 100644 index 00000000..856ffc77 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261008170022_AddSubscriptionAdapterVersions.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddSubscriptionAdapterVersions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "handler_version", + schema: "infolink", + table: "subscription", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "mapper_version", + schema: "infolink", + table: "subscription", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "receiver_version", + schema: "infolink", + table: "subscription", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "validator_version", + schema: "infolink", + table: "subscription", + type: "character varying(50)", + maxLength: 50, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "handler_version", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "mapper_version", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "receiver_version", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "validator_version", + schema: "infolink", + table: "subscription"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.Designer.cs b/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.Designer.cs new file mode 100644 index 00000000..ebeb525e --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.Designer.cs @@ -0,0 +1,3033 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009082806_AdapterSourceAccess")] + partial class AdapterSourceAccess + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.cs b/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.cs new file mode 100644 index 00000000..6289fe33 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261009082806_AdapterSourceAccess.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AdapterSourceAccess : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "adapter_source_access", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + adapter_id = table.Column(type: "character varying(200)", unicode: false, maxLength: 200, nullable: false), + version = table.Column(type: "character varying(50)", unicode: false, maxLength: 50, nullable: true), + path = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + account_id = table.Column(type: "character varying(100)", unicode: false, maxLength: 100, nullable: true), + occurred_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_adapter_source_access", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_adapter_source_access_adapter_id_occurred_on", + schema: "infolink", + table: "adapter_source_access", + columns: new[] { "adapter_id", "occurred_on" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "adapter_source_access", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.Designer.cs b/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.Designer.cs new file mode 100644 index 00000000..0aea2ac4 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.Designer.cs @@ -0,0 +1,3160 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261009141353_AdapterDrafts")] + partial class AdapterDrafts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.cs b/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.cs new file mode 100644 index 00000000..38119181 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261009141353_AdapterDrafts.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AdapterDrafts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "adapter_draft", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + adapter_id = table.Column(type: "character varying(200)", unicode: false, maxLength: 200, nullable: false), + language = table.Column(type: "character varying(20)", unicode: false, maxLength: 20, nullable: false), + kind = table.Column(type: "character varying(20)", unicode: false, maxLength: 20, nullable: true), + base_version = table.Column(type: "character varying(50)", unicode: false, maxLength: 50, nullable: true), + files_json = table.Column(type: "text", nullable: false), + files_hash = table.Column(type: "character varying(64)", unicode: false, maxLength: 64, nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + created_by = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + modified_on = table.Column(type: "timestamp with time zone", nullable: true), + modified_by = table.Column(type: "character varying(200)", maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_adapter_draft", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "adapter_release", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + adapter_id = table.Column(type: "character varying(200)", unicode: false, maxLength: 200, nullable: false), + version = table.Column(type: "character varying(50)", unicode: false, maxLength: 50, nullable: false), + action = table.Column(type: "character varying(20)", unicode: false, maxLength: 20, nullable: false), + draft_id = table.Column(type: "integer", nullable: true), + account_id = table.Column(type: "character varying(100)", unicode: false, maxLength: 100, nullable: true), + occurred_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_adapter_release", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_adapter_draft_adapter_id", + schema: "infolink", + table: "adapter_draft", + column: "adapter_id"); + + migrationBuilder.CreateIndex( + name: "ix_adapter_release_adapter_id_occurred_on", + schema: "infolink", + table: "adapter_release", + columns: new[] { "adapter_id", "occurred_on" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "adapter_draft", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "adapter_release", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.Designer.cs b/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.Designer.cs new file mode 100644 index 00000000..c20eb49b --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.Designer.cs @@ -0,0 +1,3203 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010082904_ClusterNodes")] + partial class ClusterNodes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.cs b/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.cs new file mode 100644 index 00000000..7c518ab9 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010082904_ClusterNodes.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ClusterNodes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "cluster_node", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + host = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + started_on = table.Column(type: "timestamp with time zone", nullable: false), + last_seen_on = table.Column(type: "timestamp with time zone", nullable: false), + version = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + data_sources = table.Column(type: "boolean", nullable: false), + runtimes = table.Column(type: "character varying(100)", maxLength: 100, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_cluster_node", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_cluster_node_last_seen_on", + schema: "infolink", + table: "cluster_node", + column: "last_seen_on"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cluster_node", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.Designer.cs b/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.Designer.cs new file mode 100644 index 00000000..522eac3c --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.Designer.cs @@ -0,0 +1,3224 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010084335_ApiKeyUses")] + partial class ApiKeyUses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.cs b/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.cs new file mode 100644 index 00000000..c6fafc87 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010084335_ApiKeyUses.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ApiKeyUses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api_key_use", + schema: "infolink", + columns: table => new + { + partner_id = table.Column(type: "integer", nullable: false), + key_name = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + last_used_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_api_key_use", x => new { x.partner_id, x.key_name }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api_key_use", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.Designer.cs b/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.Designer.cs new file mode 100644 index 00000000..12527d7e --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.Designer.cs @@ -0,0 +1,3271 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261010085258_OperatorActionsAndLastSignIn")] + partial class OperatorActionsAndLastSignIn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.cs b/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.cs new file mode 100644 index 00000000..2a9c01a9 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261010085258_OperatorActionsAndLastSignIn.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class OperatorActionsAndLastSignIn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "last_sign_in_on", + schema: "infolink", + table: "Accounts", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateTable( + name: "operator_action", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + action = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + target = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + detail = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + account_id = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + occurred_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_operator_action", x => x.id); + }); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + column: "last_sign_in_on", + value: null); + + migrationBuilder.CreateIndex( + name: "ix_operator_action_occurred_on", + schema: "infolink", + table: "operator_action", + column: "occurred_on"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "operator_action", + schema: "infolink"); + + migrationBuilder.DropColumn( + name: "last_sign_in_on", + schema: "infolink", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.Designer.cs b/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.Designer.cs new file mode 100644 index 00000000..f34e1d27 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.Designer.cs @@ -0,0 +1,3598 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011085208_Metrics")] + partial class Metrics + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); + + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); + + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); + + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); + + b.Property("FailedAtMapper") + .HasColumnType("integer") + .HasColumnName("failed_at_mapper"); + + b.Property("ManualRetries") + .HasColumnType("integer") + .HasColumnName("manual_retries"); + + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); + + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); + + b.ToTable("exchange_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); + + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); + + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); + + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.cs b/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.cs new file mode 100644 index 00000000..ad1af001 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261011085208_Metrics.cs @@ -0,0 +1,208 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class Metrics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "duration_ms", + schema: "infolink", + table: "xchange_result", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "handler_version", + schema: "infolink", + table: "xchange_result", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "mapper_version", + schema: "infolink", + table: "xchange_result", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "node_name", + schema: "infolink", + table: "xchange_result", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "resident_health", + schema: "infolink", + table: "cluster_node", + type: "character varying(4000)", + maxLength: 4000, + nullable: true); + + migrationBuilder.CreateTable( + name: "adapter_stats", + schema: "infolink", + columns: table => new + { + hour = table.Column(type: "timestamp with time zone", nullable: false), + node = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + adapter_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + version = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + role = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + runtime = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + calls = table.Column(type: "integer", nullable: false), + failures = table.Column(type: "integer", nullable: false), + timeouts = table.Column(type: "integer", nullable: false), + limit_stops = table.Column(type: "integer", nullable: false), + crashes = table.Column(type: "integer", nullable: false), + duration_sum_ms = table.Column(type: "bigint", nullable: false), + duration_max_ms = table.Column(type: "bigint", nullable: false), + d0 = table.Column(type: "integer", nullable: false), + d1 = table.Column(type: "integer", nullable: false), + d2 = table.Column(type: "integer", nullable: false), + d3 = table.Column(type: "integer", nullable: false), + d4 = table.Column(type: "integer", nullable: false), + d5 = table.Column(type: "integer", nullable: false), + d6 = table.Column(type: "integer", nullable: false), + d7 = table.Column(type: "integer", nullable: false), + d8 = table.Column(type: "integer", nullable: false), + d9 = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_adapter_stats", x => new { x.hour, x.node, x.adapter_id, x.version, x.role, x.runtime }); + }); + + migrationBuilder.CreateTable( + name: "error_stats", + schema: "infolink", + columns: table => new + { + hour = table.Column(type: "timestamp with time zone", nullable: false), + node = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + scope = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + scope_key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + fingerprint = table.Column(type: "character varying(16)", maxLength: 16, nullable: false), + sample = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + count = table.Column(type: "integer", nullable: false), + last_seen_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_error_stats", x => new { x.hour, x.node, x.scope, x.scope_key, x.fingerprint }); + }); + + migrationBuilder.CreateTable( + name: "exchange_stats", + schema: "infolink", + columns: table => new + { + hour = table.Column(type: "timestamp with time zone", nullable: false), + node = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + subscription_id = table.Column(type: "integer", nullable: false), + partner_id = table.Column(type: "integer", nullable: false), + total = table.Column(type: "integer", nullable: false), + succeeded = table.Column(type: "integer", nullable: false), + bad_response = table.Column(type: "integer", nullable: false), + failed = table.Column(type: "integer", nullable: false), + failed_at_mapper = table.Column(type: "integer", nullable: false), + failed_at_handler = table.Column(type: "integer", nullable: false), + retries = table.Column(type: "integer", nullable: false), + manual_retries = table.Column(type: "integer", nullable: false), + bytes_in = table.Column(type: "bigint", nullable: false), + bytes_out = table.Column(type: "bigint", nullable: false), + duration_sum_ms = table.Column(type: "bigint", nullable: false), + duration_max_ms = table.Column(type: "bigint", nullable: false), + d0 = table.Column(type: "integer", nullable: false), + d1 = table.Column(type: "integer", nullable: false), + d2 = table.Column(type: "integer", nullable: false), + d3 = table.Column(type: "integer", nullable: false), + d4 = table.Column(type: "integer", nullable: false), + d5 = table.Column(type: "integer", nullable: false), + d6 = table.Column(type: "integer", nullable: false), + d7 = table.Column(type: "integer", nullable: false), + d8 = table.Column(type: "integer", nullable: false), + d9 = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_exchange_stats", x => new { x.hour, x.node, x.subscription_id, x.partner_id }); + }); + + migrationBuilder.CreateTable( + name: "queue_snapshot", + schema: "infolink", + columns: table => new + { + at = table.Column(type: "timestamp with time zone", nullable: false), + queue_depth = table.Column(type: "bigint", nullable: false), + retry_backlog = table.Column(type: "bigint", nullable: false), + dead_letter_backlog = table.Column(type: "bigint", nullable: false), + incoming_rate = table.Column(type: "double precision", nullable: false), + ack_rate = table.Column(type: "double precision", nullable: false), + consumers = table.Column(type: "integer", nullable: false), + unhealthy_consumers = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_queue_snapshot", x => x.at); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "adapter_stats", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "error_stats", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "exchange_stats", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "queue_snapshot", + schema: "infolink"); + + migrationBuilder.DropColumn( + name: "duration_ms", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "handler_version", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "mapper_version", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "node_name", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "resident_health", + schema: "infolink", + table: "cluster_node"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.Designer.cs b/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.Designer.cs new file mode 100644 index 00000000..c4ff4fa1 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.Designer.cs @@ -0,0 +1,3698 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261011104340_ServiceAccounts")] + partial class ServiceAccounts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("Kind") + .HasColumnType("smallint") + .HasColumnName("kind"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("last_used_from"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("revoked_by"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("shown"); + + b.HasKey("Id") + .HasName("pk_service_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_service_tokens_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_service_tokens_hash"); + + b.ToTable("ServiceTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); + + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); + + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); + + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); + + b.Property("FailedAtMapper") + .HasColumnType("integer") + .HasColumnName("failed_at_mapper"); + + b.Property("ManualRetries") + .HasColumnType("integer") + .HasColumnName("manual_retries"); + + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); + + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); + + b.ToTable("exchange_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); + + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); + + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); + + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_service_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.cs b/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.cs new file mode 100644 index 00000000..6e57b6b5 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261011104340_ServiceAccounts.cs @@ -0,0 +1,91 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ServiceAccounts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "kind", + schema: "infolink", + table: "Accounts", + type: "smallint", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.CreateTable( + name: "ServiceTokens", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + account_id = table.Column(type: "integer", nullable: false), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + hash = table.Column(type: "character varying(64)", unicode: false, maxLength: 64, nullable: false), + shown = table.Column(type: "character varying(20)", unicode: false, maxLength: 20, nullable: false), + expires_on = table.Column(type: "timestamp with time zone", nullable: false), + revoked_on = table.Column(type: "timestamp with time zone", nullable: true), + revoked_by = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + last_used_on = table.Column(type: "timestamp with time zone", nullable: true), + last_used_from = table.Column(type: "character varying(64)", unicode: false, maxLength: 64, nullable: true), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + created_by = table.Column(type: "text", nullable: true), + modified_on = table.Column(type: "timestamp with time zone", nullable: true), + modified_by = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_service_tokens", x => x.id); + table.ForeignKey( + name: "fk_service_tokens_accounts_account_id", + column: x => x.account_id, + principalSchema: "infolink", + principalTable: "Accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + column: "kind", + value: (byte)0); + + migrationBuilder.CreateIndex( + name: "ix_service_tokens_account_id", + schema: "infolink", + table: "ServiceTokens", + column: "account_id"); + + migrationBuilder.CreateIndex( + name: "ix_service_tokens_hash", + schema: "infolink", + table: "ServiceTokens", + column: "hash", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ServiceTokens", + schema: "infolink"); + + migrationBuilder.DropColumn( + name: "kind", + schema: "infolink", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.Designer.cs b/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.Designer.cs new file mode 100644 index 00000000..66202002 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.Designer.cs @@ -0,0 +1,3702 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012094149_GatewayFailureAnswer")] + partial class GatewayFailureAnswer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("Kind") + .HasColumnType("smallint") + .HasColumnName("kind"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("last_used_from"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("revoked_by"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("shown"); + + b.HasKey("Id") + .HasName("pk_service_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_service_tokens_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_service_tokens_hash"); + + b.ToTable("ServiceTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FailureAnswer") + .HasColumnType("integer") + .HasColumnName("failure_answer"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); + + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); + + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); + + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); + + b.Property("FailedAtMapper") + .HasColumnType("integer") + .HasColumnName("failed_at_mapper"); + + b.Property("ManualRetries") + .HasColumnType("integer") + .HasColumnName("manual_retries"); + + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); + + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); + + b.ToTable("exchange_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); + + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); + + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); + + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_service_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.cs b/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.cs new file mode 100644 index 00000000..2a7c8ddc --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261012094149_GatewayFailureAnswer.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class GatewayFailureAnswer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "failure_answer", + schema: "infolink", + table: "api_gateway", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "failure_answer", + schema: "infolink", + table: "api_gateway"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.Designer.cs b/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.Designer.cs new file mode 100644 index 00000000..ea66a58c --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.Designer.cs @@ -0,0 +1,3710 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20261012095619_ExpectTraffic")] + partial class ExpectTraffic + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("Kind") + .HasColumnType("smallint") + .HasColumnName("kind"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasColumnName("must_change_password"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + Kind = (byte)0, + LoginMethods = (byte)2, + MustChangePassword = true, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("last_used_from"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("revoked_by"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("shown"); + + b.HasKey("Id") + .HasName("pk_service_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_service_tokens_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_service_tokens_hash"); + + b.ToTable("ServiceTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => + { + b.Property("Id") + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Changes") + .HasColumnType("text") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("character varying(36)") + .HasColumnName("correlation_id"); + + b.Property("EntityKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_key"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("entity_name"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("State") + .IsRequired() + .HasMaxLength(10) + .IsUnicode(false) + .HasColumnType("character varying(10)") + .HasColumnName("state"); + + b.Property("UserId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_audit_entries"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("ix_audit_entries_correlation_id"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_audit_entries_occurred_on"); + + b.HasIndex("EntityName", "EntityKey", "OccurredOn") + .HasDatabaseName("ix_audit_entries_entity_name_entity_key_occurred_on"); + + b.ToTable("AuditEntries", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("AcquiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_on"); + + b.Property("OwnerNode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_node"); + + b.Property("Term") + .HasColumnType("bigint") + .HasColumnName("term"); + + b.HasKey("Id") + .HasName("pk_cluster_lease"); + + b.ToTable("cluster_lease", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => + { + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("InstanceKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("instance_key"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Value") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)") + .HasColumnName("value"); + + b.HasKey("AdapterId", "InstanceKey", "Name") + .HasName("pk_adapter_state"); + + b.ToTable("adapter_state", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .HasColumnType("text") + .HasColumnName("adapter_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CpuLimitSamples") + .HasColumnType("integer") + .HasColumnName("cpu_limit_samples"); + + b.Property("CpuPercentLimit") + .HasColumnType("double precision") + .HasColumnName("cpu_percent_limit"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeduplicationWindowDays") + .HasColumnType("integer") + .HasColumnName("deduplication_window_days"); + + b.Property("HardMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("hard_memory_limit_mb"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Kind") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("LastHeartbeatOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_heartbeat_on"); + + b.Property("LastKnownState") + .HasColumnType("text") + .HasColumnName("last_known_state"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnedByNode") + .HasColumnType("text") + .HasColumnName("owned_by_node"); + + b.Property("Placement") + .HasColumnType("integer") + .HasColumnName("placement"); + + b.Property>("Properties") + .HasColumnType("hstore") + .HasColumnName("properties"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("text[]") + .HasColumnName("secret_properties"); + + b.Property("SoftMemoryLimitMb") + .HasColumnType("integer") + .HasColumnName("soft_memory_limit_mb"); + + b.HasKey("Id") + .HasName("pk_data_source"); + + b.ToTable("data_source", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("CursorColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("cursor_column"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("KeyColumn") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("key_column"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Sql") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sql"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_data_source_statement"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_data_source_statement_work_group_id"); + + b.HasIndex("DataSourceId", "Name") + .IsUnique() + .HasDatabaseName("ix_data_source_statement_data_source_id_name"); + + b.ToTable("data_source_statement", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("id"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("SeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_on"); + + b.Property("XchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_inbound_message"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_inbound_message_data_source_id"); + + b.HasIndex("SeenOn") + .HasDatabaseName("ix_inbound_message_seen_on"); + + b.ToTable("inbound_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("json") + .HasColumnName("promoted_properties"); + + b.Property("RetiredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("retired_on"); + + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthMethod") + .HasColumnType("integer") + .HasColumnName("auth_method"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FailureAnswer") + .HasColumnType("integer") + .HasColumnName("failure_answer"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("JwtAudience") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_audience"); + + b.Property("JwtIssuer") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("jwt_issuer"); + + b.Property("JwtPartnerClaim") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("jwt_partner_claim"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("PartnerKeyHeader") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("partner_key_header"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); + + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); + + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); + + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); + + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); + + b.Property("FailedAtMapper") + .HasColumnType("integer") + .HasColumnName("failed_at_mapper"); + + b.Property("ManualRetries") + .HasColumnType("integer") + .HasColumnName("manual_retries"); + + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); + + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); + + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); + + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); + + b.ToTable("exchange_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => + { + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); + + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); + + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); + + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); + + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.PrimitiveCollection("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("LoginIdentity") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("login_identity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.HasIndex("LoginIdentity") + .IsUnique() + .HasDatabaseName("ix_partner_login_identity"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.PrimitiveCollection("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ExpectTraffic") + .HasColumnType("text") + .HasColumnName("expect_traffic"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("QuietSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("quiet_since"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("RunOnBadResponses") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_responses"); + + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_subscription_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("FilesPrefix") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("files_prefix"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.PrimitiveCollection("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property>("SourceValues") + .HasColumnType("jsonb") + .HasColumnName("source_values"); + + b.Property("SourceXchangeId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("source_xchange_id"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.HasIndex("XchangeId") + .HasDatabaseName("ix_xchange_notification_xchange_id"); + + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.PrimitiveCollection("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_service_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_source_statement_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", null) + .WithMany() + .HasForeignKey("WorkGroupId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_data_source_statement_work_group_work_group_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inbound_message_data_source_data_source_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") + .WithMany() + .HasForeignKey("DataSourceId") + .HasConstraintName("fk_bus_gateway_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + + b.Navigation("DataSource"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.DataSources.DataSource", null) + .WithMany() + .HasForeignKey("DataSourceId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_data_source_data_source_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "ResponseSubscription") + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("ResponseSubscription"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.cs b/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.cs new file mode 100644 index 00000000..3139cef8 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20261012095619_ExpectTraffic.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ExpectTraffic : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "expect_traffic", + schema: "infolink", + table: "subscription", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "quiet_since", + schema: "infolink", + table: "subscription", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "expect_traffic", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "quiet_since", + schema: "infolink", + table: "subscription"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs index 07a14300..53d1e99e 100644 --- a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -71,6 +71,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("failed_login_count"); + b.Property("Kind") + .HasColumnType("smallint") + .HasColumnName("kind"); + + b.Property("LastSignInOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in_on"); + b.Property("LockoutEnd") .HasColumnType("timestamp with time zone") .HasColumnName("lockout_end"); @@ -79,6 +87,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("login_methods"); + b.Property("MicrosoftIdentity") + .HasColumnType("text") + .HasColumnName("microsoft_identity"); + b.Property("ModifiedBy") .HasColumnType("text") .HasColumnName("modified_by"); @@ -121,6 +133,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Email = "admin@Bitween.systems", EmailProvider = (byte)0, FailedLoginCount = 0, + Kind = (byte)0, LoginMethods = (byte)2, MustChangePassword = true, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", @@ -167,6 +180,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("login_method"); + b.Property("SupersededOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("superseded_on"); + b.HasKey("Id") .HasName("pk_refresh_tokens"); @@ -259,6 +276,285 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("hash"); + + b.Property("LastUsedFrom") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("last_used_from"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RevokedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("revoked_by"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Shown") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("shown"); + + b.HasKey("Id") + .HasName("pk_service_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_service_tokens_account_id"); + + b.HasIndex("Hash") + .IsUnique() + .HasDatabaseName("ix_service_tokens_hash"); + + b.ToTable("ServiceTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterDraft", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("BaseVersion") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("base_version"); + + b.Property("CreatedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("FilesHash") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("character varying(64)") + .HasColumnName("files_hash"); + + b.Property("FilesJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("files_json"); + + b.Property("Kind") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("kind"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("language"); + + b.Property("ModifiedBy") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_adapter_draft"); + + b.HasIndex("AdapterId") + .HasDatabaseName("ix_adapter_draft_adapter_id"); + + b.ToTable("adapter_draft", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("action"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("DraftId") + .HasColumnType("integer") + .HasColumnName("draft_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_release"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_release_adapter_id_occurred_on"); + + b.ToTable("adapter_release", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.AdapterSourceAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("account_id"); + + b.Property("AdapterId") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("Version") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_adapter_source_access"); + + b.HasIndex("AdapterId", "OccurredOn") + .HasDatabaseName("ix_adapter_source_access_adapter_id_occurred_on"); + + b.ToTable("adapter_source_access", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ApiKeyUse", b => + { + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("KeyName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key_name"); + + b.Property("LastUsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_on"); + + b.HasKey("PartnerId", "KeyName") + .HasName("pk_api_key_use"); + + b.ToTable("api_key_use", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b => { b.Property("Id") @@ -351,6 +647,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("cluster_lease", "infolink"); }); + modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterNode", b => + { + b.Property("Id") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("DataSources") + .HasColumnType("boolean") + .HasColumnName("data_sources"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("host"); + + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); + + b.Property("ResidentHealth") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("resident_health"); + + b.Property("Runtimes") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("runtimes"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_cluster_node"); + + b.HasIndex("LastSeenOn") + .HasDatabaseName("ix_cluster_node_last_seen_on"); + + b.ToTable("cluster_node", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b => { b.Property("AdapterId") @@ -601,6 +945,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("on"); + b.Property("RunFailures") + .HasColumnType("integer") + .HasColumnName("run_failures"); + b.HasKey("Id") .HasName("pk_delayed_retry"); @@ -659,6 +1007,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("retired_on"); + b.Property("ValidationSchema") + .HasColumnType("text") + .HasColumnName("validation_schema"); + b.HasKey("Id") .HasName("pk_document"); @@ -709,6 +1061,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("created_on"); + b.Property("FailureAnswer") + .HasColumnType("integer") + .HasColumnName("failure_answer"); + b.Property("Inactive") .HasColumnType("boolean") .HasColumnName("inactive"); @@ -802,150 +1158,498 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SubscriptionId") .HasDatabaseName("ix_api_gateway_partner_subscription_id"); - b.ToTable("api_gateway_partner", "infolink"); - }); + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DataSourceId") + .HasColumnType("integer") + .HasColumnName("data_source_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property>("EndpointProperties") + .HasColumnType("hstore") + .HasColumnName("endpoint_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DataSourceId") + .HasDatabaseName("ix_bus_gateway_data_source_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection>("SecretProperties") + .HasColumnType("jsonb") + .HasColumnName("secret_properties"); + + b.Property("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.IdempotencyKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope"); + + b.Property("XchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_idempotency_key"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_idempotency_key_created_on"); + + b.HasIndex("Scope", "Key") + .IsUnique() + .HasDatabaseName("ix_idempotency_key_scope_key"); + + b.ToTable("idempotency_key", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.AdapterStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("AdapterId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("adapter_id"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("version"); + + b.Property("Role") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("role"); + + b.Property("Runtime") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("runtime"); + + b.Property("Calls") + .HasColumnType("integer") + .HasColumnName("calls"); + + b.Property("Crashes") + .HasColumnType("integer") + .HasColumnName("crashes"); + + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); + + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); + + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); + + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); + + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); + + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); + + b.Property("D6") + .HasColumnType("integer") + .HasColumnName("d6"); + + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); + + b.Property("D8") + .HasColumnType("integer") + .HasColumnName("d8"); + + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); + + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); + + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); + + b.Property("Failures") + .HasColumnType("integer") + .HasColumnName("failures"); + + b.Property("LimitStops") + .HasColumnType("integer") + .HasColumnName("limit_stops"); + + b.Property("Timeouts") + .HasColumnType("integer") + .HasColumnName("timeouts"); + + b.HasKey("Hour", "Node", "AdapterId", "Version", "Role", "Runtime") + .HasName("pk_adapter_stats"); + + b.ToTable("adapter_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ErrorStats", b => + { + b.Property("Hour") + .HasColumnType("timestamp with time zone") + .HasColumnName("hour"); + + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); + + b.Property("Scope") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("scope"); + + b.Property("ScopeKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("scope_key"); + + b.Property("Fingerprint") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("fingerprint"); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("Count") .HasColumnType("integer") - .HasColumnName("id"); + .HasColumnName("count"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("LastSeenOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_on"); - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); + b.Property("Sample") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("sample"); - b.Property("CreatedOn") + b.HasKey("Hour", "Node", "Scope", "ScopeKey", "Fingerprint") + .HasName("pk_error_stats"); + + b.ToTable("error_stats", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Metrics.ExchangeStats", b => + { + b.Property("Hour") .HasColumnType("timestamp with time zone") - .HasColumnName("created_on"); + .HasColumnName("hour"); - b.Property("DataSourceId") - .HasColumnType("integer") - .HasColumnName("data_source_id"); + b.Property("Node") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node"); - b.Property("DocumentId") + b.Property("SubscriptionId") .HasColumnType("integer") - .HasColumnName("document_id"); + .HasColumnName("subscription_id"); - b.Property("Endpoint") - .HasColumnType("text") - .HasColumnName("endpoint"); + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); - b.Property>("EndpointProperties") - .HasColumnType("hstore") - .HasColumnName("endpoint_properties"); + b.Property("BadResponse") + .HasColumnType("integer") + .HasColumnName("bad_response"); - b.Property("Inactive") - .HasColumnType("boolean") - .HasColumnName("inactive"); + b.Property("BytesIn") + .HasColumnType("bigint") + .HasColumnName("bytes_in"); - b.Property("ModifiedBy") - .HasColumnType("text") - .HasColumnName("modified_by"); + b.Property("BytesOut") + .HasColumnType("bigint") + .HasColumnName("bytes_out"); - b.Property("ModifiedOn") - .HasColumnType("timestamp with time zone") - .HasColumnName("modified_on"); + b.Property("D0") + .HasColumnType("integer") + .HasColumnName("d0"); - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasColumnName("name"); + b.Property("D1") + .HasColumnType("integer") + .HasColumnName("d1"); - b.HasKey("Id") - .HasName("pk_bus_gateway"); + b.Property("D2") + .HasColumnType("integer") + .HasColumnName("d2"); - b.HasIndex("DataSourceId") - .HasDatabaseName("ix_bus_gateway_data_source_id"); + b.Property("D3") + .HasColumnType("integer") + .HasColumnName("d3"); - b.HasIndex("DocumentId") - .HasDatabaseName("ix_bus_gateway_document_id"); + b.Property("D4") + .HasColumnType("integer") + .HasColumnName("d4"); - b.ToTable("bus_gateway", "infolink"); - }); + b.Property("D5") + .HasColumnType("integer") + .HasColumnName("d5"); - modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("D6") .HasColumnType("integer") - .HasColumnName("id"); + .HasColumnName("d6"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("D7") + .HasColumnType("integer") + .HasColumnName("d7"); - b.Property("BusGatewayId") + b.Property("D8") .HasColumnType("integer") - .HasColumnName("bus_gateway_id"); + .HasColumnName("d8"); - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); + b.Property("D9") + .HasColumnType("integer") + .HasColumnName("d9"); - b.Property("CreatedOn") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_on"); + b.Property("DurationMaxMs") + .HasColumnType("bigint") + .HasColumnName("duration_max_ms"); - b.Property("MatchExpression") - .HasColumnType("text") - .HasColumnName("match_expression"); + b.Property("DurationSumMs") + .HasColumnType("bigint") + .HasColumnName("duration_sum_ms"); - b.Property("ModifiedBy") - .HasColumnType("text") - .HasColumnName("modified_by"); + b.Property("Failed") + .HasColumnType("integer") + .HasColumnName("failed"); - b.Property("ModifiedOn") - .HasColumnType("timestamp with time zone") - .HasColumnName("modified_on"); + b.Property("FailedAtHandler") + .HasColumnType("integer") + .HasColumnName("failed_at_handler"); - b.Property("PartnerId") + b.Property("FailedAtMapper") .HasColumnType("integer") - .HasColumnName("partner_id"); + .HasColumnName("failed_at_mapper"); - b.Property("SubscriptionId") + b.Property("ManualRetries") .HasColumnType("integer") - .HasColumnName("subscription_id"); + .HasColumnName("manual_retries"); - b.HasKey("Id") - .HasName("pk_bus_gateway_route"); + b.Property("Retries") + .HasColumnType("integer") + .HasColumnName("retries"); - b.HasIndex("BusGatewayId") - .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + b.Property("Succeeded") + .HasColumnType("integer") + .HasColumnName("succeeded"); - b.HasIndex("PartnerId") - .HasDatabaseName("ix_bus_gateway_route_partner_id"); + b.Property("Total") + .HasColumnType("integer") + .HasColumnName("total"); - b.HasIndex("SubscriptionId") - .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + b.HasKey("Hour", "Node", "SubscriptionId", "PartnerId") + .HasName("pk_exchange_stats"); - b.ToTable("bus_gateway_route", "infolink"); + b.ToTable("exchange_stats", "infolink"); }); - modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + modelBuilder.Entity("SW.Bitween.Domain.Metrics.QueueSnapshot", b => { - b.Property("Id") - .HasColumnType("text") - .HasColumnName("id"); + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); - b.Property("Name") - .HasColumnType("text") - .HasColumnName("name"); + b.Property("AckRate") + .HasColumnType("double precision") + .HasColumnName("ack_rate"); - b.PrimitiveCollection>("SecretProperties") - .HasColumnType("jsonb") - .HasColumnName("secret_properties"); + b.Property("Consumers") + .HasColumnType("integer") + .HasColumnName("consumers"); - b.Property>("Values") - .HasColumnType("jsonb") - .HasColumnName("values"); + b.Property("DeadLetterBacklog") + .HasColumnType("bigint") + .HasColumnName("dead_letter_backlog"); - b.HasKey("Id") - .HasName("pk_global_adapter_values_set"); + b.Property("IncomingRate") + .HasColumnType("double precision") + .HasColumnName("incoming_rate"); - b.ToTable("global_adapter_values_set", "infolink"); + b.Property("QueueDepth") + .HasColumnType("bigint") + .HasColumnName("queue_depth"); + + b.Property("RetryBacklog") + .HasColumnType("bigint") + .HasColumnName("retry_backlog"); + + b.Property("UnhealthyConsumers") + .HasColumnType("integer") + .HasColumnName("unhealthy_consumers"); + + b.HasKey("At") + .HasName("pk_queue_snapshot"); + + b.ToTable("queue_snapshot", "infolink"); }); modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => @@ -1054,6 +1758,103 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("on_hold_xchange", "infolink"); }); + modelBuilder.Entity("SW.Bitween.Domain.OperatorAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("detail"); + + b.Property("OccurredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_on"); + + b.Property("Target") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("target"); + + b.HasKey("Id") + .HasName("pk_operator_action"); + + b.HasIndex("OccurredOn") + .HasDatabaseName("ix_operator_action_occurred_on"); + + b.ToTable("operator_action", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ClaimedUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_until"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Headers") + .HasColumnType("text") + .HasColumnName("headers"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last_error"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message_type"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.HasKey("Id") + .HasName("pk_outbox_message"); + + b.HasIndex("PublishedOn") + .HasDatabaseName("ix_outbox_message_published_on"); + + b.ToTable("outbox_message", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => { b.Property("Id") @@ -1063,7 +1864,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property>("AdapterProperties") + b.Property("AdapterProperties") .HasColumnType("jsonb") .HasColumnName("adapter_properties"); @@ -1303,6 +2104,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("aggregation_target"); + b.Property("AutoPauseAfterFailures") + .HasColumnType("integer") + .HasColumnName("auto_pause_after_failures"); + b.Property("CategoryId") .HasColumnType("integer") .HasColumnName("category_id"); @@ -1327,15 +2132,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); + b.Property("ExpectTraffic") + .HasColumnType("text") + .HasColumnName("expect_traffic"); + b.Property("HandlerId") .HasMaxLength(200) .HasColumnType("character varying(200)") .HasColumnName("handler_id"); - b.Property>("HandlerProperties") + b.Property("HandlerProperties") .HasColumnType("jsonb") .HasColumnName("handler_properties"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + b.Property("Inactive") .HasColumnType("boolean") .HasColumnName("inactive"); @@ -1353,10 +2167,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("mapper_id"); - b.Property>("MapperProperties") + b.Property("MapperProperties") .HasColumnType("jsonb") .HasColumnName("mapper_properties"); + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + b.Property("MatchExpression") .HasColumnType("text") .HasColumnName("match_expression"); @@ -1371,10 +2190,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("partner_id"); + b.Property("PausedAutomatically") + .HasColumnType("boolean") + .HasColumnName("paused_automatically"); + b.Property("PausedOn") .HasColumnType("timestamp with time zone") .HasColumnName("paused_on"); + b.Property("QuietSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("quiet_since"); + b.Property("ReceiveOn") .HasColumnType("timestamp with time zone") .HasColumnName("receive_on"); @@ -1384,10 +2211,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("receiver_id"); - b.Property>("ReceiverProperties") + b.Property("ReceiverProperties") .HasColumnType("jsonb") .HasColumnName("receiver_properties"); + b.Property("ReceiverVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("receiver_version"); + b.Property("ResponseMessageTypeName") .HasMaxLength(500) .HasColumnType("character varying(500)") @@ -1405,6 +2237,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("run_on_bad_responses"); + b.Property("RunningSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("running_since"); + b.Property("Temporary") .HasColumnType("boolean") .HasColumnName("temporary"); @@ -1418,10 +2254,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("validator_id"); - b.Property>("ValidatorProperties") + b.Property("ValidatorProperties") .HasColumnType("jsonb") .HasColumnName("validator_properties"); + b.Property("ValidatorVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("validator_version"); + b.Property("WorkGroupId") .HasColumnType("integer") .HasColumnName("work_group_id"); @@ -1554,7 +2395,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("handler_id"); - b.Property>("HandlerProperties") + b.Property("HandlerProperties") .HasColumnType("jsonb") .HasColumnName("handler_properties"); @@ -1587,7 +2428,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)") .HasColumnName("mapper_id"); - b.Property>("MapperProperties") + b.Property("MapperProperties") .HasColumnType("jsonb") .HasColumnName("mapper_properties"); @@ -1726,6 +2567,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("success"); + b.Property("Suppressed") + .HasColumnType("boolean") + .HasColumnName("suppressed"); + b.Property("XchangeId") .HasMaxLength(50) .IsUnicode(false) @@ -1738,6 +2583,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("XchangeId") .HasDatabaseName("ix_xchange_notification_xchange_id"); + b.HasIndex("NotifierId", "FinishedOn") + .HasDatabaseName("ix_xchange_notification_notifier_id_finished_on"); + b.ToTable("xchange_notification", "infolink"); }); @@ -1777,6 +2625,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("attempt_number"); + b.Property("DurationMs") + .HasColumnType("integer") + .HasColumnName("duration_ms"); + b.Property("Exception") .HasColumnType("text") .HasColumnName("exception"); @@ -1785,6 +2637,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("finished_on"); + b.Property("HandlerVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("handler_version"); + + b.Property("MapperVersion") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("mapper_version"); + + b.Property("NodeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("node_name"); + b.Property("OutputBad") .HasColumnType("boolean") .HasColumnName("output_bad"); @@ -2427,6 +3294,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_refresh_tokens_accounts_account_id"); }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.ServiceToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_service_tokens_accounts_account_id"); + }); + modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b => { b.HasOne("SW.Bitween.Domain.DataSources.DataSource", "DataSource") @@ -2552,6 +3429,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(500)") .HasColumnName("key"); + b1.Property("KeyPrefix") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("key_prefix"); + b1.Property("Name") .IsRequired() .HasMaxLength(500) diff --git a/SW.Bitween.SampleConfigurableAdapter/LICENSE b/SW.Bitween.SampleConfigurableAdapter/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.SampleConfigurableAdapter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj index 459aaec4..4e8de255 100644 --- a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj +++ b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj @@ -5,6 +5,6 @@ SW.Bitween.SampleConfigurableAdapter - + diff --git a/SW.Bitween.SampleHandler/LICENSE b/SW.Bitween.SampleHandler/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.SampleHandler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj index 7b2fe4ad..29d84f27 100644 --- a/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj +++ b/SW.Bitween.SampleHandler/SW.Bitween.SampleHandler.csproj @@ -7,7 +7,7 @@ - + diff --git a/SW.Bitween.SampleMapper/LICENSE b/SW.Bitween.SampleMapper/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.SampleMapper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj index cf7b2a45..ad299e09 100644 --- a/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj +++ b/SW.Bitween.SampleMapper/SW.Bitween.SampleMapper.csproj @@ -7,7 +7,7 @@ - + diff --git a/SW.Bitween.SampleResidentHandler/Handler.cs b/SW.Bitween.SampleResidentHandler/Handler.cs index d3f49237..93dcf0ea 100644 --- a/SW.Bitween.SampleResidentHandler/Handler.cs +++ b/SW.Bitween.SampleResidentHandler/Handler.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.SampleResidentHandler; /// -/// A handler that STAYS RUNNING, used exactly where a classic one would be — as a subscription's +/// A handler that STAYS RUNNING, used exactly where a classic one would be - as a subscription's /// handler or mapper. /// /// It implements IInfolinkHandler like any other, so nothing about the pipeline's contract changes. @@ -15,7 +15,7 @@ namespace SW.Bitween.SampleResidentHandler; /// hold an open connection or a warm cache instead of paying for them on every message. /// /// It counts what it has handled and reports that on the heartbeat, so a test can prove the SAME -/// process served more than one message — which is the whole distinction from the classic +/// process served more than one message - which is the whole distinction from the classic /// lifecycle, and not something a single call could show. /// public class Handler : IResidentAdapter, IInfolinkHandler diff --git a/SW.Bitween.SampleResidentHandler/LICENSE b/SW.Bitween.SampleResidentHandler/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.SampleResidentHandler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.SampleResidentHandler/SW.Bitween.SampleResidentHandler.csproj b/SW.Bitween.SampleResidentHandler/SW.Bitween.SampleResidentHandler.csproj index f9d68f29..be9ea3b5 100644 --- a/SW.Bitween.SampleResidentHandler/SW.Bitween.SampleResidentHandler.csproj +++ b/SW.Bitween.SampleResidentHandler/SW.Bitween.SampleResidentHandler.csproj @@ -2,12 +2,12 @@ Exe - net8.0 + net10.0 SW.Bitween.SampleResidentHandler - + diff --git a/SW.Bitween.SampleValidator/LICENSE b/SW.Bitween.SampleValidator/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.SampleValidator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj index 2d89b3f6..07b7e777 100644 --- a/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj +++ b/SW.Bitween.SampleValidator/SW.Bitween.SampleValidator.csproj @@ -8,7 +8,7 @@ - + diff --git a/SW.Bitween.Sdk/LICENSE b/SW.Bitween.Sdk/LICENSE new file mode 100644 index 00000000..6cf7c9ed --- /dev/null +++ b/SW.Bitween.Sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2026 Simplify9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SW.Bitween.Sdk/Model/Account.cs b/SW.Bitween.Sdk/Model/Account.cs index e819d272..594986e8 100644 --- a/SW.Bitween.Sdk/Model/Account.cs +++ b/SW.Bitween.Sdk/Model/Account.cs @@ -15,7 +15,7 @@ public class CreateAccountModel /// /// Legacy coarse role. Nullable on purpose: when it was a plain int, a request that omitted it - /// sent 0 — which is Admin — so a member added with no roles came out an administrator. + /// sent 0 - which is Admin - so a member added with no roles came out an administrator. /// public int? Role { get; set; } @@ -29,7 +29,7 @@ public class UpdateAccountModel /// /// Legacy coarse role. Nullable on purpose: when it was a plain int, a request that omitted it - /// sent 0 — which is Admin. Only supplied values are applied, and only with users.edit. + /// sent 0 - which is Admin. Only supplied values are applied, and only with users.edit. /// public int? Role { get; set; } } @@ -64,6 +64,13 @@ public class AccountModel public bool Disabled { get; set; } public DateTime CreatedOn { get; set; } + + /// When the member last signed in (password, Microsoft or the CLI); null if never since this was kept. + public DateTime? LastSignInOn { get; set; } + + /// Whether a Microsoft identity is bound to the account. + public bool MicrosoftLinked { get; set; } + public List Roles { get; set; } = []; // Non-null and in the future => the account is currently locked out. @@ -78,7 +85,7 @@ public class ProfileModel : AccountModel /// /// The account still has the password it was created with, so its token grants nothing until /// that is changed. Here as well as on the sign-in response because a reload has no sign-in - /// response to read — without it, refreshing the page lands on an app with every screen denied + /// response to read - without it, refreshing the page lands on an app with every screen denied /// and no explanation. /// public bool MustChangePassword { get; set; } diff --git a/SW.Bitween.Sdk/Model/ApiGateway.cs b/SW.Bitween.Sdk/Model/ApiGateway.cs index 93abb792..6ed2d5dd 100644 --- a/SW.Bitween.Sdk/Model/ApiGateway.cs +++ b/SW.Bitween.Sdk/Model/ApiGateway.cs @@ -15,10 +15,31 @@ public class ApiGatewayCreate : IName /// /// How callers prove who they are. Left out on create, the gateway takes partner keys; - /// left out on update, it keeps what it has — so a save that only renames or pauses the + /// left out on update, it keeps what it has - so a save that only renames or pauses the /// gateway can't quietly switch it back to partner keys. /// public ApiGatewayAuthentication? Authentication { get; set; } + + /// + /// What a sync call is told when its delivery fails. Left out on create, a new gateway answers + /// Accurate; left out on update, it keeps what it has. Gateways made before this existed answer + /// Legacy, as they always did. + /// + public GatewayFailureAnswer? FailureAnswer { get; set; } + } + + /// What a sync gateway call is told when the delivery behind it fails. + public enum GatewayFailureAnswer + { + /// 400 with no body, whatever happened: what every gateway answered before. + Legacy = 0, + + /// + /// 202 when Bitween has scheduled a retry (it has the message: don't send it again), 422 when + /// the message couldn't be mapped, 502 when the delivery failed and won't be retried. Each with + /// the exchange id and a short reason. + /// + Accurate = 1, } /// How the callers of an API gateway prove who they are. diff --git a/SW.Bitween.Sdk/Model/Audit.cs b/SW.Bitween.Sdk/Model/Audit.cs index 2d24db3e..0d17722c 100644 --- a/SW.Bitween.Sdk/Model/Audit.cs +++ b/SW.Bitween.Sdk/Model/Audit.cs @@ -39,7 +39,7 @@ public class AuditEntryModel /// /// The account's display name at the time it is read, or null for a change made with no signed-in - /// user — a bus consumer or a scheduled job. Resolved on read rather than stored, so it is + /// user - a bus consumer or a scheduled job. Resolved on read rather than stored, so it is /// blank rather than wrong once an account is deleted. /// public string? UserDisplayName { get; set; } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs index 06a9a22a..7bfb7a0e 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs @@ -33,7 +33,7 @@ public readonly record struct RetryBudgetClaim(bool Granted, bool JustExhausted) /// /// /// The total is a ceiling shared by every message that hits the group, so the count cannot -/// live on the message being evaluated — it needs a store that outlives a single xchange. +/// live on the message being evaluated - it needs a store that outlives a single xchange. /// The implementation decides the scope: the production one keys by integration + group and /// persists, while counts only for one dry-run. /// diff --git a/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs b/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs index ea8eb55a..cbc05a1a 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs @@ -50,6 +50,6 @@ public enum RetryAction /// Schedule a retry according to the group's . Allow, - /// Hard-block this error — no retry even if a budget would otherwise allow it. + /// Hard-block this error - no retry even if a budget would otherwise allow it. Block, } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs index 92eaa763..82329d0e 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs @@ -54,7 +54,7 @@ public abstract class Matcher /// /// Returns true when satisfies this matcher's condition. - /// Implementations must never throw — malformed input should return false. + /// Implementations must never throw - malformed input should return false. /// public abstract bool IsMatch(string content); } @@ -62,7 +62,7 @@ public abstract class Matcher // ── Text matchers (Error and BadResult) ─────────────────────────────────────── /// -/// Matches when the failure text contains a literal substring — the exception text for +/// Matches when the failure text contains a literal substring - the exception text for /// , the response body for . /// public class ContainsMatcher : Matcher @@ -84,7 +84,7 @@ public override bool IsMatch(string content) => } /// -/// Matches when the failure text satisfies a regular expression — the exception text for +/// Matches when the failure text satisfies a regular expression - the exception text for /// , the response body for . /// public class RegexMatcher : Matcher @@ -138,7 +138,7 @@ public override bool Supports(XchangeResultType resultType) => public required string Value { get; init; } /// - /// When true (default) the full stack trace — including inner exceptions — is + /// When true (default) the full stack trace - including inner exceptions - is /// scanned. When false only the first type name in the text is checked. /// public bool IncludeInner { get; init; } = true; diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs index bc4d2113..e8c413e1 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs @@ -18,7 +18,7 @@ public enum RetryAlertMode /// Send through this level's own handler, ignoring anything configured above it. Send, - /// Send nothing, and stop the walk — an alert configured above is deliberately suppressed here. + /// Send nothing, and stop the walk - an alert configured above is deliberately suppressed here. Silent, } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs index d862ceb6..43d453b4 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.Model; /// JSON payload and vice-versa. /// /// -/// Matcher logic: OR — the group fires as soon as any single matcher returns +/// Matcher logic: OR - the group fires as soon as any single matcher returns /// true. Matchers incompatible with the current result type are silently skipped. /// /// @@ -73,7 +73,7 @@ public class RetryGroup /// public string? AlertHandlerId { get; init; } - /// That adapter's own settings — api key, recipients, subject. + /// That adapter's own settings - api key, recipients, subject. public Dictionary? AlertHandlerProperties { get; init; } } @@ -92,8 +92,8 @@ public class RetryBudget /// Hard ceiling on the total number of group-level retries across all messages, counted per /// subscription so one shared policy does not let a single noisy subscription spend everyone's /// allowance. Prevents a burst of failures from hammering the downstream. It is not a rate over a - /// rolling window: the count only falls once it has been reached — the subscription's next success - /// then lifts it — or when somebody resets it by hand. + /// rolling window: the count only falls once it has been reached - the subscription's next success + /// then lifts it - or when somebody resets it by hand. /// /// /// Null means no group-wide ceiling: only applies, nothing is diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs index c76d8207..ffed253b 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs @@ -119,13 +119,13 @@ public class RetryDecision /// /// The group that matched this failure, or null when none did. Set on blocked - /// decisions too — internal callers (attempt tracking, exhaustion alerts) need to know which + /// decisions too - internal callers (attempt tracking, exhaustion alerts) need to know which /// group refused a failure, not only which group allowed one. /// public RetryGroup? MatchedGroup { get; private init; } /// - /// Name of the that allowed a retry, or null when blocked — + /// Name of the that allowed a retry, or null when blocked - /// including when a group matched but refused. A refusal by a matched group and a failure no /// group was ever configured to catch should look the same to a caller that only cares whether /// something is retrying, which is what is for instead. @@ -139,7 +139,7 @@ public class RetryDecision /// public bool BudgetJustExhausted { get; private init; } - /// Creates an Allow decision — a retry will be scheduled after . + /// Creates an Allow decision - a retry will be scheduled after . public static RetryDecision Allow(TimeSpan delay, RetryGroup group) => new() { ShouldRetry = true, @@ -148,7 +148,7 @@ public class RetryDecision Reason = $"Allowed by group '{group.Name}'" }; - /// Creates a Block decision — no retry will be scheduled. + /// Creates a Block decision - no retry will be scheduled. public static RetryDecision Block(string reason, RetryGroup? group = null, bool budgetJustExhausted = false) => new() { diff --git a/SW.Bitween.Sdk/Model/BusGateway.cs b/SW.Bitween.Sdk/Model/BusGateway.cs index 82ec2b06..5435e68e 100644 --- a/SW.Bitween.Sdk/Model/BusGateway.cs +++ b/SW.Bitween.Sdk/Model/BusGateway.cs @@ -13,7 +13,7 @@ public class BusGatewayCreate : IName public bool Inactive { get; set; } /// - /// Null is the INTERNAL bus — the only behaviour that existed before, and still the + /// Null is the INTERNAL bus - the only behaviour that existed before, and still the /// default. Set it and this gateway is fed by an external broker instead, through the /// resident adapter that data source names. /// @@ -28,7 +28,7 @@ public class BusGatewayCreate : IName /// /// Per-gateway overrides passed to the adapter. NOT YET CONSUMED by either bundled - /// adapter — see BusGateway.EndpointProperties on the entity. + /// adapter - see BusGateway.EndpointProperties on the entity. /// public Dictionary EndpointProperties { get; set; } = new(); } diff --git a/SW.Bitween.Sdk/Model/DataSource.cs b/SW.Bitween.Sdk/Model/DataSource.cs index 27c819e0..8e9d090c 100644 --- a/SW.Bitween.Sdk/Model/DataSource.cs +++ b/SW.Bitween.Sdk/Model/DataSource.cs @@ -21,7 +21,7 @@ public class DataSourceCreate : IName /// /// How many nodes may run this source's adapter: Auto, Exclusive or PerNode. /// - /// Auto — the default — follows from : a broker connection is exclusive + /// Auto - the default - follows from : a broker connection is exclusive /// because two nodes consuming one queue is duplicate processing, and everything else is /// per-node because a connection pool held by a single node leaves every other node unable to /// use it. Override only for the case that crosses over: a relational source registered for @@ -58,7 +58,7 @@ public class DataSourceCreate : IName /// /// Sustained CPU ceiling as a percentage of the whole node. Zero leaves the host default. - /// One pegged core on a sixteen-core node is about 6%, not 100% — see the entity's remarks. + /// One pegged core on a sixteen-core node is about 6%, not 100% - see the entity's remarks. /// public double CpuPercentLimit { get; set; } @@ -121,7 +121,7 @@ public class DataSourceTestResult /// Stage name to outcome, in the order the adapter attempted them. public List Stages { get; set; } = new(); - /// Whatever the adapter chose to report — endpoint, queue depths, visibility timeout. + /// Whatever the adapter chose to report - endpoint, queue depths, visibility timeout. public Dictionary Details { get; set; } = new(); } @@ -138,7 +138,7 @@ public class DataSourceTestStage /// What the connection is doing right now, as opposed to what it was configured to do. /// /// Read live from the resident adapter host rather than from the data source row, because the row -/// only carries what the last reconcile happened to write back — a summary, thirty seconds stale at +/// only carries what the last reconcile happened to write back - a summary, thirty seconds stale at /// worst. This is the heartbeat itself: the counters the adapter keeps, the queue depths it can /// see, and what the host observes about the process without needing the adapter's cooperation. /// @@ -151,7 +151,7 @@ public class DataSourceTelemetry /// public bool RunningHere { get; set; } - /// Which node holds the connection, from the data source row — filled in even when it is not this one. + /// Which node holds the connection, from the data source row - filled in even when it is not this one. public string? OwnedByNode { get; set; } // ---------------------------------------------------------------- adapter-reported @@ -167,7 +167,7 @@ public class DataSourceTelemetry /// /// Whatever the adapter chose to report: per-queue depth, messages received and acknowledged, - /// prefetch, the endpoint it is connected to. Untyped on purpose — Bitween does not model any + /// prefetch, the endpoint it is connected to. Untyped on purpose - Bitween does not model any /// broker's telemetry any more than it models its topology. /// public Dictionary Details { get; set; } = new(); @@ -188,7 +188,7 @@ public class DataSourceTelemetry public DateTime? LastHeartbeatOn { get; set; } - /// What the adapter says it can do — the commands the UI could offer against it. + /// What the adapter says it can do - the commands the UI could offer against it. public List Commands { get; set; } = new(); } @@ -205,7 +205,7 @@ public class DataSourceInspectRequest /// Strings, and deliberately so: this crosses two serialization boundaries to reach an /// adapter that binds it to a typed request, and "200"/"true" coerce cleanly while an /// untyped object graph would not survive the trip unchanged. Nothing here can widen what - /// the command does — the allow-list decides that, and every command on it is read-only. + /// the command does - the allow-list decides that, and every command on it is read-only. /// public Dictionary? Arguments { get; set; } } @@ -231,7 +231,7 @@ public class DataSourceInspectResult /// What a bus provider accepts, as the adapter itself declares it. /// /// This exists so the UI can render a data source form without knowing anything about brokers. The -/// alternative — the one this replaced — is a table of fields, defaults and allowed values kept by +/// alternative - the one this replaced - is a table of fields, defaults and allowed values kept by /// hand in the front end, which is a copy of a contract it does not own and cannot be told when it /// changes. /// @@ -243,7 +243,7 @@ public class DataSourceProviderDescriptor public string Label { get; set; } = null!; /// - /// Which DataSourceKind this provider produces — Broker, Relational, Document, ObjectStore or + /// Which DataSourceKind this provider produces - Broker, Relational, Document, ObjectStore or /// Http. A data source is not only a broker connection: a resident adapter that holds a /// database session is one too, and it must never be offered as a bus gateway's source. /// @@ -252,7 +252,7 @@ public class DataSourceProviderDescriptor /// /// How many nodes may run this source's adapter: Auto, Exclusive or PerNode. /// - /// Auto — the default — follows from : a broker connection is exclusive + /// Auto - the default - follows from : a broker connection is exclusive /// because two nodes consuming one queue is duplicate processing, and everything else is /// per-node because a connection pool held by a single node leaves every other node unable to /// use it. Override only for the case that crosses over: a relational source registered for @@ -273,7 +273,7 @@ public class DataSourceProviderSetting public const string NumberType = "number"; public const string BooleanType = "boolean"; - /// The property name the adapter binds by — this is the data source property key. + /// The property name the adapter binds by - this is the data source property key. public string Name { get; set; } = null!; /// string, number or boolean. Coarse on purpose: it picks an input, nothing more. @@ -286,7 +286,7 @@ public class DataSourceProviderSetting /// Null when the adapter declares none. public string? Default { get; set; } - /// When set, the only legal values — the UI offers these instead of free text. + /// When set, the only legal values - the UI offers these instead of free text. /// Null when the setting is free text rather than a fixed choice. public string[]? AllowedValues { get; set; } diff --git a/SW.Bitween.Sdk/Model/DataSourceStatement.cs b/SW.Bitween.Sdk/Model/DataSourceStatement.cs index b7aabe06..0ec46c31 100644 --- a/SW.Bitween.Sdk/Model/DataSourceStatement.cs +++ b/SW.Bitween.Sdk/Model/DataSourceStatement.cs @@ -12,7 +12,7 @@ public class DataSourceStatementCreate : IName { /// /// Which connection it belongs to. In the body rather than the route because the resource is - /// addressed as /datasourcestatements — a keyed create would collide with the update handler, + /// addressed as /datasourcestatements - a keyed create would collide with the update handler, /// which is what POST /datasourcestatements/{id} already means. /// public int DataSourceId { get; set; } @@ -36,7 +36,7 @@ public class DataSourceStatementCreate : IName public bool Inactive { get; set; } /// - /// Only for a statement a receiver polls with: the column carrying the cursor — the + /// Only for a statement a receiver polls with: the column carrying the cursor - the /// incrementing id or the modified-at timestamp. It describes what this query returns, so it /// belongs to the statement rather than to each subscription reading it. /// @@ -62,7 +62,7 @@ public class DataSourceStatementRow : DataSourceStatementUpdate public string? WorkGroupName { get; set; } /// - /// How many subscriptions name this statement. Zero is the interesting value — it is the only + /// How many subscriptions name this statement. Zero is the interesting value - it is the only /// reliable way to tell dead SQL from SQL that is merely quiet, and deleting is refused above /// zero. /// @@ -95,7 +95,7 @@ public class DataSourceStatementUsageEntry public int SubscriptionId { get; set; } public string SubscriptionName { get; set; } = null!; - /// Handler, Mapper or Receiver — which slot's properties name it. + /// Handler, Mapper or Receiver - which slot's properties name it. public string Role { get; set; } = null!; /// What that slot will do with it: query, execute or call. diff --git a/SW.Bitween.Sdk/Model/DelayedRetryModel.cs b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs index a7e299cd..7eb0946c 100644 --- a/SW.Bitween.Sdk/Model/DelayedRetryModel.cs +++ b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs @@ -25,7 +25,7 @@ public class DelayedRetryRow /// /// The shared retry policy the subscription currently points at. Null when the - /// subscription carries an inline CustomRetryPolicy instead — a delayed retry + /// subscription carries an inline CustomRetryPolicy instead - a delayed retry /// can only be scheduled by one or the other, so null means "look on the subscription". /// Reflects the policy as it stands now, not necessarily the one that set . /// diff --git a/SW.Bitween.Sdk/Model/Document.cs b/SW.Bitween.Sdk/Model/Document.cs index fbabb47c..258e4b50 100644 --- a/SW.Bitween.Sdk/Model/Document.cs +++ b/SW.Bitween.Sdk/Model/Document.cs @@ -18,7 +18,7 @@ public enum DocumentFormat Csv = 2, /// - /// Anything Bitween has no format for — custom adapters send whatever they like. Treated + /// Anything Bitween has no format for - custom adapters send whatever they like. Treated /// exactly as . /// Other = 3 @@ -48,6 +48,12 @@ public class DocumentCreate : IName /// Null leaves the existing promoted properties alone; an empty collection clears them. /// public ICollection? PromotedProperties { get; set; } + + /// + /// JSON Schema for a JSON type, XSD for an XML one, that what arrives must match. Null + /// leaves the existing schema alone; empty removes it. + /// + public string? ValidationSchema { get; set; } } public class DocumentUpdate : DocumentCreate @@ -55,7 +61,7 @@ public class DocumentUpdate : DocumentCreate public int Id { get; set; } } - /// Empty on purpose — retiring toggles, so there is nothing to send. + /// Empty on purpose - retiring toggles, so there is nothing to send. public class DocumentRetire { } diff --git a/SW.Bitween.Sdk/Model/ExpectTraffic.cs b/SW.Bitween.Sdk/Model/ExpectTraffic.cs new file mode 100644 index 00000000..bfead1aa --- /dev/null +++ b/SW.Bitween.Sdk/Model/ExpectTraffic.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace SW.Bitween.Model +{ + /// + /// How often a subscription should see an exchange. When none arrives for longer than + /// inside the window, the subscription is flagged as gone quiet and the + /// notifiers watching it for failures are told: a partner that stops sending fails nothing. + /// + public class ExpectTraffic + { + /// At least one exchange this often, in minutes. Five or more. + public int EveryMinutes { get; set; } + + /// Sun, Mon, Tue, Wed, Thu, Fri, Sat. Null or empty means every day. + public ICollection? Days { get; set; } + + /// + /// Local time of day the window opens, HH:mm. Set with , or neither for the + /// whole day. A window can't run past midnight. + /// + public string? From { get; set; } + + /// Local time of day the window closes, HH:mm, later than . + public string? To { get; set; } + + /// IANA time zone the days and times are in, e.g. Asia/Riyadh. Null means UTC. + public string? TimeZone { get; set; } + } +} diff --git a/SW.Bitween.Sdk/Model/Metrics.cs b/SW.Bitween.Sdk/Model/Metrics.cs new file mode 100644 index 00000000..a0eedcda --- /dev/null +++ b/SW.Bitween.Sdk/Model/Metrics.cs @@ -0,0 +1,19 @@ +namespace SW.Bitween.Model; + +/// A metrics query over the last days (1 to 90; 7 when not given). +public class MetricsWindowRequest +{ + public int? Days { get; set; } +} + +public class AdapterMetricsRequest +{ + public string Id { get; set; } + public int? Days { get; set; } +} + +public class SubscriptionMetricsRequest +{ + public int Id { get; set; } + public int? Days { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/Partner.cs b/SW.Bitween.Sdk/Model/Partner.cs index e22825ee..d41d4198 100644 --- a/SW.Bitween.Sdk/Model/Partner.cs +++ b/SW.Bitween.Sdk/Model/Partner.cs @@ -11,7 +11,7 @@ public class PartnerCreate : IName /// /// Referenced from adapter fields as {{partner.KEY}}. Accepted at creation so a - /// partner can be made complete in one call — the UI creates partners from + /// partner can be made complete in one call - the UI creates partners from /// inside other flows, where a follow-up update that fails would leave a /// partner whose adapters resolve nothing. /// @@ -37,7 +37,7 @@ public class PartnerRow : PartnerUpdate public int? SubscriptionsCount { get; set; } public int? Keys { get; set; } /// - /// The names of the partner's adapter properties — never their values, which + /// The names of the partner's adapter properties - never their values, which /// can be secrets. Names alone are enough to count them in a list and to offer /// them as {{partner.x}} reference tokens when configuring an adapter. /// diff --git a/SW.Bitween.Sdk/Model/Permissions.cs b/SW.Bitween.Sdk/Model/Permissions.cs index 542bd97a..0068bbaa 100644 --- a/SW.Bitween.Sdk/Model/Permissions.cs +++ b/SW.Bitween.Sdk/Model/Permissions.cs @@ -21,7 +21,10 @@ public static class Monitoring { public const string View = "monitoring.view"; - /// Delete leftover queues nothing reads, along with whatever they hold. + /// + /// Delete leftover queues nothing reads, along with whatever they hold, and send + /// dead-lettered messages back to be tried again. + /// public const string Operate = "monitoring.operate"; } @@ -100,8 +103,8 @@ public static class DataSources /// /// Separate from on purpose. Statements have to live on the - /// connection — SQL in a subscription's adapter properties would have partner values templated - /// into it — but that must not mean editing SQL requires the right that also changes the + /// connection - SQL in a subscription's adapter properties would have partner values templated + /// into it - but that must not mean editing SQL requires the right that also changes the /// credentials. Someone configuring their own integration gets these; only whoever owns the /// connection gets DataSources.Edit. /// @@ -141,6 +144,14 @@ public static class Users public const string Delete = "users.delete"; } + public static class ServiceAccounts + { + public const string View = "service-accounts.view"; + public const string Create = "service-accounts.create"; + public const string Edit = "service-accounts.edit"; + public const string Delete = "service-accounts.delete"; + } + public static class Roles { public const string View = "roles.view"; @@ -156,13 +167,25 @@ public static class Settings } /// - /// Read-only by design. Nothing edits or deletes an audit entry — a trail that could be + /// Read-only by design. Nothing edits or deletes an audit entry - a trail that could be /// rewritten by the people it records would not be worth keeping. /// public static class Audit { public const string View = "audit.view"; } + + public static class AdapterSource + { + /// Read the source an adapter version was published with, and compare versions. + public const string View = "adapter-source.view"; + + /// Write adapters in Bitween's editor: drafts, building, testing and trying them. + public const string Edit = "adapter-source.edit"; + + /// Publish a version from the editor, and make a published version the current one. + public const string Operate = "adapter-source.operate"; + } } public class PermissionActionModel @@ -205,19 +228,19 @@ private static PermissionAreaModel Area(string id, string label, string group, s public static readonly List Areas = [ - // ——— Operate ——— + // --- Operate --- Area("exchanges", "Exchanges", "Operate", "Every message that flows through Bitween.", (View, "Browse exchanges, payloads and traces."), (Operate, "Retry or resubmit failed exchanges.")), Area("monitoring", "Queue health", "Operate", "Live message-queue throughput and consumers.", (View, "See queue health and rates."), - (Operate, "Delete leftover queues nothing reads, and the messages in them.")), + (Operate, "Delete leftover queues nothing reads, and the messages in them; send dead letters back to be tried again.")), Area("dashboard", "Dashboard", "Operate", "Traffic and health overview (reached from the logo).", (View, "See the dashboard.")), - // ——— Subscriptions ——— + // --- Subscriptions --- Area("subscriptions", "Subscriptions", "Subscriptions", "The configured pipelines that process exchanges.", (View, "Browse subscriptions and their configuration."), (Create, "Create subscriptions."), @@ -262,7 +285,7 @@ private static PermissionAreaModel Area(string id, string label, string group, s (Edit, "Change gateways and routes."), (Delete, "Delete bus gateways.")), - // ——— Configuration ——— + // --- Configuration --- Area("data-sources", "Data sources", "Configuration", "Connections to external brokers that bus gateways can read from.", (View, "Browse data sources and their connection health."), @@ -292,13 +315,21 @@ private static PermissionAreaModel Area(string id, string label, string group, s (Edit, "Change retry policies."), (Delete, "Delete retry policies.")), - // ——— Administration ——— + // --- Administration --- Area("users", "Members", "Administration", "The people who can sign in to this Bitween instance.", (View, "See the member list."), (Create, "Invite new members."), (Edit, "Change members' roles, disable accounts, reset passwords."), (Delete, "Remove members.")), + // Administration, like members: a token is a way in, and whoever issues one decides what it can do. + Area("service-accounts", "Service accounts", "Administration", + "Accounts for programs, such as a job applying configuration, that sign in with tokens instead of a password.", + (View, "See service accounts and their tokens (never a token's value)."), + (Create, "Create service accounts and issue their tokens."), + (Edit, "Change a service account's roles, disable it, revoke tokens."), + (Delete, "Remove service accounts.")), + Area("roles", "Roles", "Administration", "What each kind of member is allowed to do.", (View, "See roles and their permissions."), (Create, "Create roles."), @@ -313,14 +344,22 @@ private static PermissionAreaModel Area(string id, string label, string group, s // account and role changes, which is exactly what someone covering their tracks would edit. Area("audit", "Audit trail", "Administration", "Who changed what, and when, across every configuration entity.", - (View, "Browse the audit trail.")) + (View, "Browse the audit trail.")), + + // Administration, like the trail: source is our partners' code and our own, and some of + // it is licensed to us rather than ours to show every member. + Area("adapter-source", "Adapter code", "Administration", + "The code each adapter version was published with, and adapters written in Bitween.", + (View, "Read an adapter's source and compare its versions."), + (Edit, "Write Python and JavaScript adapters in the editor: drafts, building, testing and trying them."), + (Operate, "Publish versions from the editor, and make a published version the one that runs.")) ]; /// Every valid permission key. public static readonly HashSet AllKeys = Areas.SelectMany(a => a.Actions.Select(x => $"{a.Id}.{x.Id}")).ToHashSet(); - /// Drops anything not in the catalog — stale keys left over from a removed area. + /// Drops anything not in the catalog - stale keys left over from a removed area. public static List Sanitize(IEnumerable keys) => (keys ?? []).Where(AllKeys.Contains).Distinct().ToList(); diff --git a/SW.Bitween.Sdk/Model/Retention.cs b/SW.Bitween.Sdk/Model/Retention.cs index 6d6d3d60..67ccbf41 100644 --- a/SW.Bitween.Sdk/Model/Retention.cs +++ b/SW.Bitween.Sdk/Model/Retention.cs @@ -18,7 +18,7 @@ public class RetentionStatusRequest public bool Refresh { get; set; } } -/// How long exchanges and their files are kept, and what the settings will do — now or as proposed. +/// How long exchanges and their files are kept, and what the settings will do - now or as proposed. public class RetentionStatus { public StorageRulesInfo Storage { get; set; } = null!; diff --git a/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs index 1319fdf3..511db20c 100644 --- a/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs +++ b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.Model; /// subscription, meaning failures matching that group have stopped being retried. /// /// -/// Sent once per subscription and group, and not again until the budget is reset — unlike +/// Sent once per subscription and group, and not again until the budget is reset - unlike /// , which is sent per exchange. /// public class RetryBudgetExhaustedNotification @@ -24,7 +24,7 @@ public class RetryBudgetExhaustedNotification /// Null when the subscription carries an inline policy rather than a named one. public string? PolicyName { get; set; } - /// The group whose budget is spent — the condition that has stopped being retried. + /// The group whose budget is spent - the condition that has stopped being retried. public string GroupName { get; set; } = null!; /// The ceiling that was reached. diff --git a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs index ac529c74..5218ff37 100644 --- a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs +++ b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs @@ -14,7 +14,7 @@ public class RetryPolicyCreate /// public string? AlertHandlerId { get; set; } - /// That adapter's own settings — api key, recipients, subject. + /// That adapter's own settings - api key, recipients, subject. public Dictionary? AlertHandlerProperties { get; set; } } @@ -46,7 +46,7 @@ public class RetryPolicyRow /// when a budget runs out is "did anyone get told?", and that needs both. /// /// -/// A row exists for every pair, including subscriptions that have never failed — an alert override +/// A row exists for every pair, including subscriptions that have never failed - an alert override /// has to be configurable before the first failure, not after. Those rows carry the group's ceiling /// with nothing spent against it, and a null . /// @@ -71,7 +71,7 @@ public class RetryGroupUsageRow public DateTime? LastAttemptOn { get; set; } /// - /// When the exhaustion alert was raised, or null if the budget still has room — or ran out + /// When the exhaustion alert was raised, or null if the budget still has room - or ran out /// before alerts existed. /// public DateTime? ExhaustedNotifiedOn { get; set; } @@ -82,8 +82,8 @@ public class RetryGroupUsageRow /// /// Separate from because they are different facts: /// claiming the alert is what stops it firing twice, and it is claimed before the send is - /// attempted. A send through a customer-configured adapter can fail — a wrong password, a - /// refused TLS handshake — and the counter records none of that. Reporting only the claim + /// attempted. A send through a customer-configured adapter can fail - a wrong password, a + /// refused TLS handshake - and the counter records none of that. Reporting only the claim /// tells the reader someone was told when nobody was, which is the one thing this page /// must never do. /// @@ -189,7 +189,7 @@ public class RetryAlertOverrideSave public Dictionary? AlertHandlerProperties { get; set; } } -/// Empty request body — the subject is identified by the route key. +/// Empty request body - the subject is identified by the route key. public class RetryPolicyUsageRequest { } @@ -221,10 +221,10 @@ public class RetryPolicyResetUsage /// public class TestRetryPolicyRequest { - /// The draft groups to test — not necessarily the persisted policy's groups. + /// The draft groups to test - not necessarily the persisted policy's groups. public List Groups { get; set; } = []; - /// Error or BadResult — Success is never retried and is rejected. + /// Error or BadResult - Success is never retried and is rejected. public XchangeResultType ResultType { get; set; } /// Exception text for Error, or the raw JSON response body for BadResult. diff --git a/SW.Bitween.Sdk/Model/Roles.cs b/SW.Bitween.Sdk/Model/Roles.cs index fd0d788f..f8d25467 100644 --- a/SW.Bitween.Sdk/Model/Roles.cs +++ b/SW.Bitween.Sdk/Model/Roles.cs @@ -14,7 +14,7 @@ public class RoleCreate public class RoleUpdate : RoleCreate; -/// One shape for both the roles list and a single role — the UI shows the same fields. +/// One shape for both the roles list and a single role - the UI shows the same fields. public class RoleRow { public int Id { get; set; } diff --git a/SW.Bitween.Sdk/Model/ServiceAccounts.cs b/SW.Bitween.Sdk/Model/ServiceAccounts.cs new file mode 100644 index 00000000..1ffb85d7 --- /dev/null +++ b/SW.Bitween.Sdk/Model/ServiceAccounts.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +namespace SW.Bitween.Model; + +/// Trades a service token for a session: POST /api/accounts/servicetoken. +public class ServiceTokenSignInModel +{ + public string Token { get; set; } = null!; +} + +public class SearchServiceAccountsModel +{ +} + +public class CreateServiceAccountModel +{ + public string Name { get; set; } = null!; + public List RoleIds { get; set; } = []; +} + +public class UpdateServiceAccountModel +{ + public string Name { get; set; } = null!; + public List RoleIds { get; set; } = []; +} + +public class SetServiceAccountDisabledModel +{ + public bool Disabled { get; set; } +} + +public class CreateServiceTokenModel +{ + /// What the token is for, to tell it apart: "lab apply job". + public string Name { get; set; } = null!; + + /// 1 to 365; 90 when left out. + public int? ExpiresInDays { get; set; } +} + +public class RevokeServiceTokenModel +{ + public int TokenId { get; set; } +} + +public class ServiceTokenModel +{ + public int Id { get; set; } + public string Name { get; set; } = null!; + + /// The token's first characters, "bwt_3fK9aB2x", never the token. + public string Shown { get; set; } = null!; + + public DateTime CreatedOn { get; set; } + public string? CreatedBy { get; set; } + public DateTime ExpiresOn { get; set; } + public DateTime? RevokedOn { get; set; } + public DateTime? LastUsedOn { get; set; } + public string? LastUsedFrom { get; set; } + + /// active, expired or revoked. + public string Status { get; set; } = null!; +} + +public class ServiceAccountModel +{ + public int Id { get; set; } + public string Name { get; set; } = null!; + public bool Disabled { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime? LastSignInOn { get; set; } + public List Roles { get; set; } = []; + public List Tokens { get; set; } = []; +} + +/// A token just made: the only answer that ever carries its value. +public class IssuedServiceTokenModel : ServiceTokenModel +{ + public string Token { get; set; } = null!; +} diff --git a/SW.Bitween.Sdk/Model/Settings.cs b/SW.Bitween.Sdk/Model/Settings.cs index 7d4dc831..0bd1f24c 100644 --- a/SW.Bitween.Sdk/Model/Settings.cs +++ b/SW.Bitween.Sdk/Model/Settings.cs @@ -2,7 +2,7 @@ namespace SW.Bitween.Model; /// /// One setting as the settings page sees it: the catalog's definition plus whatever value is in -/// effect. A secret's value never leaves the server — only reveals that +/// effect. A secret's value never leaves the server - only reveals that /// one is set. /// public class SettingRow @@ -17,7 +17,7 @@ public class SettingRow /// "string", "number", "boolean" or "color". public string Kind { get; set; } = null!; - /// The product default — what a reset returns this setting to. Empty for secrets. + /// The product default - what a reset returns this setting to. Empty for secrets. public string DefaultValue { get; set; } = null!; /// The stored value. Always null for secrets, whose value never leaves the server. @@ -28,12 +28,12 @@ public class SettingRow /// True when the stored value differs from the product default, i.e. a reset would do something. public bool Overridden { get; set; } - /// Whether the effective value is non-empty — lets the UI mask a secret that is set. + /// Whether the effective value is non-empty - lets the UI mask a secret that is set. public bool HasValue { get; set; } /// /// Whether the UI may write this setting. False for every environment-owned setting, and for a - /// secret on an instance with no Bitween:SettingsEncryptionKey — there's nowhere safe to + /// secret on an instance with no Bitween:SettingsEncryptionKey - there's nowhere safe to /// store that, so it stays configuration-only. /// public bool Editable { get; set; } diff --git a/SW.Bitween.Sdk/Model/Subscription.cs b/SW.Bitween.Sdk/Model/Subscription.cs index 9ba887cb..2b86c4ab 100644 --- a/SW.Bitween.Sdk/Model/Subscription.cs +++ b/SW.Bitween.Sdk/Model/Subscription.cs @@ -63,7 +63,7 @@ public class SubscriptionLastRunModel : SubscriptionRunModel { public int SubscriptionId { get; set; } - /// Finished runs in the recent window — in-progress runs are excluded, being neither. + /// Finished runs in the recent window - in-progress runs are excluded, being neither. public int RecentTotal { get; set; } /// How many of succeeded. @@ -82,7 +82,7 @@ public class SearchSubscriptionLastRunsModel /// /// One execution of a Receiving subscription's own receive step, recorded directly by - /// ReceivingJob — independent of the scheduler's own run history, which only knows + /// ReceivingJob - independent of the scheduler's own run history, which only knows /// whether Execute() threw (it never does; the receive step's own failures are caught /// and reported here instead, alongside the successes and no-op checks history never covers). /// @@ -129,7 +129,7 @@ public class SourcePathsRequest } /// - /// The paths in the last document a subscription received — what a response or bus gateway + /// The paths in the last document a subscription received - what a response or bus gateway /// subscription it feeds can read as source values. /// public class SourcePathsModel @@ -154,7 +154,7 @@ public class SourcePath } /// - /// Whether a scheduled subscription will actually fire — read from the scheduler + /// Whether a scheduled subscription will actually fire - read from the scheduler /// itself, not from what Bitween thinks it configured. The two can disagree, and /// when they do it is silent: the UI shows a healthy job that never runs. /// @@ -171,7 +171,7 @@ public class SubscriptionScheduleHealthModel /// Worst state across the subscription's triggers: Normal, Paused, Blocked, Error, Complete, or Missing. public string State { get; set; } = null!; - /// The scheduler's own next fire time — computed from the cron, independently of Subscription.ReceiveOn. + /// The scheduler's own next fire time - computed from the cron, independently of Subscription.ReceiveOn. public DateTime? NextFireOn { get; set; } /// @@ -196,10 +196,10 @@ public abstract class SubscriptionCreateUpdateBase : IName } /// - /// How a subscription is configured — everything a person chooses. Shared by create and + /// How a subscription is configured - everything a person chooses. Shared by create and /// update so the two can't drift: a field here is applied by both, through the same code. /// - /// Runtime state (what the subscription has since done — failure counts, last exception, + /// Runtime state (what the subscription has since done - failure counts, last exception, /// next fire time) deliberately lives on only. None of it /// is meaningful for something that doesn't exist yet. /// @@ -216,7 +216,17 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase public string? ValidatorId { get; set; } /// - /// Which data source this subscription's adapters connect through — a database connection, + /// The published version of the handler to run, e.g. 1.4.0. Null - what a caller that + /// predates versions sends - follows whichever version is current. Native adapters have no + /// versions. The same goes for the other three slots. + /// + public string? HandlerVersion { get; set; } + public string? MapperVersion { get; set; } + public string? ReceiverVersion { get; set; } + public string? ValidatorVersion { get; set; } + + /// + /// Which data source this subscription's adapters connect through - a database connection, /// typically. Null keeps the old behaviour, where an adapter carries its own connection /// settings in its properties. /// @@ -234,8 +244,8 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase public ICollection DocumentFilter { get; set; } = []; /// - /// Null and empty mean different things: null leaves the existing schedules alone — - /// a create that mentions no schedule is the "empty subscription" call — while an + /// Null and empty mean different things: null leaves the existing schedules alone - + /// a create that mentions no schedule is the "empty subscription" call - while an /// empty collection asks for none, which SetSchedules refuses on a Receiving type. /// public ICollection? Schedules { get; set; } @@ -243,7 +253,7 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase /// /// A response subscription to create and hand the response to, in the same transaction as - /// this one — instead of , never with it. Its + /// this one - instead of , never with it. Its /// DocumentId is the information type of the responses it runs on. /// public InlineIntegrationCreate? NewResponseSubscription { get; set; } @@ -260,6 +270,20 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase public int? RetryPolicyId { get; set; } public CustomRetryPolicy? CustomRetryPolicy { get; set; } + /// + /// Pause the subscription after this many failed deliveries in a row, so a target that is + /// down stops being hammered and what arrives meanwhile waits on hold. Null - the default - + /// never pauses. + /// + public int? AutoPauseAfterFailures { get; set; } + + /// + /// Flag the subscription as gone quiet when no exchange arrives this often, inside a window. + /// Null - the default - expects nothing. Replaced like the other fields here: an update + /// without it removes it. + /// + public ExpectTraffic? ExpectTraffic { get; set; } + /// /// Aggregation only: which file of each collected exchange the roll-up links to. /// @@ -274,7 +298,7 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase /// /// Creates a subscription complete, in one transaction. Everything beyond - /// and is optional — a caller + /// and is optional - a caller /// that sends only those still gets the empty, inactive subscription it always did. /// /// @@ -321,6 +345,8 @@ public class SubscriptionUpdate : SubscriptionConfiguration /// Null while the subscription is healthy. public string? LastException { get; set; } public DateTime? PausedOn { get; set; } + /// Paused by , not by a person. + public bool PausedAutomatically { get; set; } /// Both null when the subscription is in no category. public string? CategoryCode { get; set; } @@ -330,5 +356,12 @@ public class SubscriptionUpdate : SubscriptionConfiguration public class SubscriptionGet : SubscriptionUpdate { public SubscriptionType Type { get; set; } + + /// + /// When the subscription was found quiet against its ; + /// null while traffic arrives as expected. Here rather than on + /// so that sending a read back can't set it. + /// + public DateTime? QuietSince { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.Sdk/Model/Xchange.cs b/SW.Bitween.Sdk/Model/Xchange.cs index ad3eeba4..ace4874b 100644 --- a/SW.Bitween.Sdk/Model/Xchange.cs +++ b/SW.Bitween.Sdk/Model/Xchange.cs @@ -66,7 +66,7 @@ public class XchangeBulkRetry : XchangeRetry, IXchangeSelection public string Filter { get; set; } /// - /// Exchanges to leave out of a selection — the rows unticked after + /// Exchanges to leave out of a selection - the rows unticked after /// selecting everything. /// public List ExcludeIds { get; set; } @@ -118,7 +118,7 @@ public class XchangeBulkRetryPlan /// /// true when the selection is past . Nothing else is filled in - /// then — resolving thousands of chains to describe a request that will be refused + /// then - resolving thousands of chains to describe a request that will be refused /// costs more than the answer is worth. /// public bool OverLimit { get; set; } @@ -143,7 +143,7 @@ public class XchangeBulkRetryPlan /// /// A selected exchange that had already been retried, paired with the newest attempt in its - /// chain — the one a retry actually runs, since retrying the selected one again is refused. + /// chain - the one a retry actually runs, since retrying the selected one again is refused. /// public class XchangeRetrySubstitution { @@ -154,8 +154,8 @@ public class XchangeRetrySubstitution public class XchangeRetrySkip { /// - /// The exchange the skip is about: the one selected, or — once it had already been - /// retried — the later attempt that stood in for it. + /// The exchange the skip is about: the one selected, or - once it had already been + /// retried - the later attempt that stood in for it. /// public string Id { get; set; } @@ -164,7 +164,7 @@ public class XchangeRetrySkip public class XchangeRetryTreeRequest { - /// Any exchange in the chain — the answer is the same whichever one is asked about. + /// Any exchange in the chain - the answer is the same whichever one is asked about. public string Id { get; set; } } @@ -290,9 +290,66 @@ public class XchangeRow /// /// /// Carried on the row rather than left to the retry-tree endpoint so that the common - /// case — an exchange with no retries either side of it — needs no second request at + /// case - an exchange with no retries either side of it - needs no second request at /// all. Costs one index lookup per returned row, on the index over RetryFor. /// public bool HasRetry { get; set; } } -} \ No newline at end of file + + /// + /// The exchange list's filter, summarised: how many exchanges are in each status, and when they + /// started. Returned by GET /api/xchanges/summary, which takes the same filters the search + /// does. + /// + /// + /// Any status filter is ignored: this is the breakdown the status tabs show, each tab's count + /// under every other filter the list has, so narrowing to one status first would leave three of + /// them reading zero. + /// + public class XchangeSummary + { + /// How many exchanges match, by status. Each is capped like the search's total. + public XchangeStatusCounts Counts { get; set; } = new XchangeStatusCounts(); + + /// + /// When the matching exchanges started, by status, in fixed buckets of + /// each - contiguous and zero-filled from the start of the + /// filter's date range to its end. Empty when the filter has no start date: with no range + /// there is nothing to divide into buckets but the whole history. + /// + public List Buckets { get; set; } = new List(); + + /// How long each bucket is, in minutes; 0 when there are no buckets. + public int BucketMinutes { get; set; } + } + + /// + /// Exchanges by status, defined exactly as the list's status filter defines them. A count of + /// 10001 means "more than 10,000": counting stops there, as the search's total does. + /// + public class XchangeStatusCounts + { + /// Still running: no result yet. + public int Processing { get; set; } + + /// Finished, and the response (if any) was not marked bad. + public int Success { get; set; } + + /// Finished, but the response was marked bad. + public int BadResponse { get; set; } + + public int Failed { get; set; } + } + + /// One bucket of : exchanges that started in it, by status. + public class XchangeSummaryBucket + { + /// When the bucket starts, in UTC. It ends where the next one starts. + public DateTime Start { get; set; } + + public int Processing { get; set; } + public int Success { get; set; } + public int BadResponse { get; set; } + public int Failed { get; set; } + } +} diff --git a/SW.Bitween.Sdk/Model/XchangeResultNotification.cs b/SW.Bitween.Sdk/Model/XchangeResultNotification.cs index ac4954b0..b92dd2e3 100644 --- a/SW.Bitween.Sdk/Model/XchangeResultNotification.cs +++ b/SW.Bitween.Sdk/Model/XchangeResultNotification.cs @@ -17,5 +17,18 @@ public class XchangeResultNotification public string SubscriptionName { get; set; } = null!; public string CorrelationId { get; set; } = null!; public DateTime StartedOn { get; set; } + + /// + /// How many notifications like this one - same notifier, subscription and outcome - were + /// held back since the last one sent, so an outage reads as one message with a count. + /// + public int SuppressedSinceLast { get; set; } + + /// + /// Null for the result of an exchange. "gone-quiet" when a subscription saw no exchange for + /// longer than it expects, and "back" when exchanges arrive again; neither has an exchange, + /// so and are an id made up for the alert. + /// + public string? Event { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.TransportTests/Adapters.cs b/SW.Bitween.TransportTests/Adapters.cs new file mode 100644 index 00000000..09f289cd --- /dev/null +++ b/SW.Bitween.TransportTests/Adapters.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.NativeAdapters; + +namespace SW.Bitween.TransportTests; + +/// Native adapters as the app builds them: registered by AddNativeAdapters, configured by settings. +public static class Adapters +{ + static readonly ServiceProvider Services = Build(); + + static ServiceProvider Build() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddMemoryCache(); + services.AddNativeAdapters(); + return services.BuildServiceProvider(); + } + + public static T Create(IDictionary settings) where T : INativeAdapter + { + var scope = Services.CreateScope(); + var adapter = scope.ServiceProvider.GetServices().OfType().First(); + adapter.InitializeStartupValues(settings); + return adapter; + } +} diff --git a/SW.Bitween.TransportTests/AzureBlobTests.cs b/SW.Bitween.TransportTests/AzureBlobTests.cs new file mode 100644 index 00000000..1b721bac --- /dev/null +++ b/SW.Bitween.TransportTests/AzureBlobTests.cs @@ -0,0 +1,120 @@ +using System.Text; +using Azure.Storage.Blobs; +using SW.Bitween.NativeAdapters.AzureBlobReceiver; +using SW.Bitween.NativeAdapters.AzureBlobUploadHandler; +using SW.PrimitiveTypes; +using Testcontainers.Azurite; +using Xunit; + +namespace SW.Bitween.TransportTests; + +public sealed class AzuriteFixture : IAsyncLifetime +{ + readonly AzuriteContainer container = new AzuriteBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:latest") + // The service version the SDK speaks may be newer than the emulator knows. + .WithCommand("--skipApiVersionCheck") + .Build(); + + public string ConnectionString => container.GetConnectionString(); + + public Task InitializeAsync() => container.StartAsync(); + + /// A private container - no public access - as customers' containers are. + public async Task ContainerAsync() + { + var container = new BlobContainerClient(ConnectionString, $"c{Guid.NewGuid():N}"[..20]); + await container.CreateAsync(); + return container; + } + + public async Task DisposeAsync() => await container.DisposeAsync(); +} + +/// The Azure Blob upload handler and receiver against a real Blob service (Azurite). +public class AzureBlobTests(AzuriteFixture azurite) : IClassFixture +{ + Dictionary Settings(BlobContainerClient container, params (string Key, string Value)[] more) + { + var settings = new Dictionary + { + ["ConnectionString"] = azurite.ConnectionString, + ["ContainerName"] = container.Name, + }; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + static async Task> NamesAsync(BlobContainerClient container) + { + var names = new List(); + await foreach (var blob in container.GetBlobsAsync()) names.Add(blob.Name); + return names.OrderBy(n => n).ToList(); + } + + static async Task TextAsync(BlobContainerClient container, string name) => + (await container.GetBlobClient(name).DownloadContentAsync()).Value.Content.ToString(); + + static Task PutAsync(BlobContainerClient container, string name, string text) => + container.GetBlobClient(name).UploadAsync(new BinaryData(Encoding.UTF8.GetBytes(text)), overwrite: true); + + [Fact] + public async Task The_upload_handler_writes_a_generated_or_fixed_name() + { + var container = await azurite.ContainerAsync(); + + var generated = await Adapters.Create(Settings(container, ("FileExtension", "csv"))) + .Handle(new XchangeFile("a,b\n1,2", "export.csv")); + Assert.Matches(@"^\d{14}_[0-9a-f]{32}\.csv$", generated.Data); + Assert.Equal("a,b\n1,2", await TextAsync(container, generated.Data)); + + var fixedSettings = Settings(container, ("FileName", "daily/orders.json")); + await Adapters.Create(fixedSettings).Handle(new XchangeFile("first")); + await Adapters.Create(fixedSettings).Handle(new XchangeFile("second")); + Assert.Equal("second", await TextAsync(container, "daily/orders.json")); + Assert.Equal(2, (await NamesAsync(container)).Count); + } + + [Fact] + public async Task The_receiver_reads_only_its_folder_and_moves_each_file_once_received() + { + var container = await azurite.ContainerAsync(); + await PutAsync(container, "incoming/a.json", "{\"n\":\"a\"}"); + await PutAsync(container, "incoming/sub/b.json", "{\"n\":\"b\"}"); + await PutAsync(container, "incoming-archive/old.json", "a sibling, not inside the folder"); + + var receiver = Adapters.Create(Settings(container, + ("FolderName", "incoming"), ("DeleteMovesFileTo", "processed"))); + await receiver.Initialize(); + + var names = (await receiver.ListFiles()).OrderBy(n => n).ToList(); + Assert.Equal(["incoming/a.json", "incoming/sub/b.json"], names); + + foreach (var name in names) + { + Assert.StartsWith("{\"n\":", (await receiver.GetFile(name)).Data); + await receiver.DeleteFile(name); + } + // Received twice - a retried run - is not an error. + await receiver.DeleteFile(names[0]); + + Assert.Equal(["incoming-archive/old.json", "processed/a.json", "processed/sub/b.json"], await NamesAsync(container)); + Assert.Equal("{\"n\":\"b\"}", await TextAsync(container, "processed/sub/b.json")); + } + + [Fact] + public async Task The_receiver_lists_in_batches_and_deletes_when_there_is_nowhere_to_move() + { + var container = await azurite.ContainerAsync(); + for (var i = 0; i < 3; i++) await PutAsync(container, $"in/{i}.txt", $"file {i}"); + + var receiver = Adapters.Create(Settings(container, ("FolderName", "in"), ("BatchSize", "2"))); + await receiver.Initialize(); + + var batch = (await receiver.ListFiles()).ToList(); + Assert.Equal(2, batch.Count); + foreach (var name in batch) await receiver.DeleteFile(name); + + Assert.Single(await NamesAsync(container)); + } +} diff --git a/SW.Bitween.TransportTests/CircuitBreakerTests.cs b/SW.Bitween.TransportTests/CircuitBreakerTests.cs new file mode 100644 index 00000000..22c6d01b --- /dev/null +++ b/SW.Bitween.TransportTests/CircuitBreakerTests.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.TransportTests; + +/// +/// A partner that has stopped answering properly, on a loopback port of its own so its circuit is +/// nobody else's: 503 or no answer at all until told to recover. It counts what reaches it. +/// +public sealed class AilingPartner : IAsyncLifetime +{ + WebApplication app = null!; + public string Url { get; private set; } = null!; + public int Calls; + public volatile bool Healthy; + + public async Task InitializeAsync() + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + app = builder.Build(); + + app.Use(async (context, next) => + { + Interlocked.Increment(ref Calls); + if (Healthy) context.Response.StatusCode = 200; + else await next(); + }); + app.MapPost("/down", () => Results.Text("maintenance", statusCode: 503)); + app.MapPost("/hang", async () => + { + await Task.Delay(TimeSpan.FromSeconds(10)); + return Results.Ok(); + }); + + await app.StartAsync(); + Url = app.Services.GetRequiredService().Features.Get()!.Addresses.First(); + } + + public async Task DisposeAsync() => await app.DisposeAsync(); +} + +/// The HTTP handler's circuit breaker against a real HTTP server that keeps failing. +public class CircuitBreakerTests(AilingPartner partner) : IClassFixture +{ + sealed class Clock : TimeProvider + { + public DateTimeOffset Now = DateTimeOffset.UtcNow; + public override DateTimeOffset GetUtcNow() => Now; + } + + readonly Clock clock = new(); + + /// The adapters as the app registers them, with a breaker of three failures and a clock the test turns. + Func Handlers() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddMemoryCache(); + services.AddSingleton(clock); + services.AddNativeAdapters(circuitBreaker: _ => new CircuitBreakerSettings(Failures: 3, WindowSeconds: 60, OpenSeconds: 60)); + var provider = services.BuildServiceProvider(); + + return (path, timeout) => + { + // A scope per delivery, as the app has: the breaker still has to be the same one. + var handler = provider.CreateScope().ServiceProvider.GetServices().OfType().First(); + handler.InitializeStartupValues(new Dictionary { ["Url"] = partner.Url + path, ["TimeoutSeconds"] = timeout.ToString() }); + return handler; + }; + } + + [Fact] + public async Task After_three_503s_the_handler_fails_at_once_without_calling_and_recovers_after_the_cool_down() + { + partner.Healthy = false; + var handler = Handlers(); + + for (var i = 0; i < 3; i++) + { + var failure = await Assert.ThrowsAsync(() => handler("/down", 5).Handle(new XchangeFile("{}"))); + Assert.StartsWith("503: ServiceUnavailable: maintenance", failure.Message); + } + var before = partner.Calls; + + var started = DateTime.UtcNow; + var open = await Assert.ThrowsAsync(() => handler("/down", 5).Handle(new XchangeFile("{}"))); + Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(1)); + Assert.StartsWith($"503: ServiceUnavailable: circuit open for {partner.Url} (3 failures in a row; trying again after ", open.Message); + Assert.Equal(before, partner.Calls); + + partner.Healthy = true; + await Assert.ThrowsAsync(() => handler("/down", 5).Handle(new XchangeFile("{}"))); + Assert.Equal(before, partner.Calls); + + clock.Now = clock.Now.AddSeconds(60); + await handler("/down", 5).Handle(new XchangeFile("{}")); + await handler("/down", 5).Handle(new XchangeFile("{}")); + Assert.Equal(before + 2, partner.Calls); + } + + [Fact] + public async Task After_three_timeouts_the_handler_stops_waiting_on_a_partner_that_never_answers() + { + partner.Healthy = false; + var handler = Handlers(); + + for (var i = 0; i < 3; i++) + await Assert.ThrowsAsync(() => handler("/hang", 1).Handle(new XchangeFile("{}"))); + var before = partner.Calls; + + var started = DateTime.UtcNow; + await Assert.ThrowsAsync(() => handler("/hang", 1).Handle(new XchangeFile("{}"))); + Assert.True(DateTime.UtcNow - started < TimeSpan.FromMilliseconds(500), "failed without waiting for the timeout"); + Assert.Equal(before, partner.Calls); + + // The trial fails too, and the circuit opens again for another cool-down. + clock.Now = clock.Now.AddSeconds(60); + await Assert.ThrowsAsync(() => handler("/hang", 1).Handle(new XchangeFile("{}"))); + var reopened = await Assert.ThrowsAsync(() => handler("/hang", 1).Handle(new XchangeFile("{}"))); + Assert.Contains("the trial call failed", reopened.Message); + Assert.Equal(before + 1, partner.Calls); + } +} diff --git a/SW.Bitween.TransportTests/HttpTests.cs b/SW.Bitween.TransportTests/HttpTests.cs new file mode 100644 index 00000000..1c59199e --- /dev/null +++ b/SW.Bitween.TransportTests/HttpTests.cs @@ -0,0 +1,215 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.HttpReceiver; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.TransportTests; + +/// What a partner's HTTP endpoint received. +public record Received(string Method, string Path, string Body, string? ContentType, IReadOnlyDictionary Headers); + +/// +/// A partner's HTTP endpoints, served by a real Kestrel on a loopback port: it records each request +/// and answers as the path asks - success, a bad request, too many requests, a failure, slowness, +/// and the token endpoints the adapters sign in with. +/// +public sealed class PartnerServer : IAsyncLifetime +{ + WebApplication app = null!; + public string Url { get; private set; } = null!; + public ConcurrentQueue Requests { get; } = new(); + + public async Task InitializeAsync() + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + app = builder.Build(); + + app.Use(async (context, next) => + { + using var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + Requests.Enqueue(new Received(context.Request.Method, context.Request.Path + context.Request.QueryString, body, + context.Request.ContentType, + context.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase))); + context.Items["body"] = body; + await next(); + }); + + app.MapPost("/orders/{id}", (string id) => Results.Json(new { accepted = id }, statusCode: 201)); + app.MapPost("/bad", () => Results.Json(new { error = "order has no lines" }, statusCode: 400)); + app.MapPost("/busy", (HttpContext c) => + { + c.Response.Headers.RetryAfter = "30"; + return Results.StatusCode(429); + }); + app.MapPost("/broken", () => Results.Text("upstream exploded", statusCode: 502)); + app.MapPost("/slow", async () => + { + await Task.Delay(TimeSpan.FromSeconds(10)); + return Results.Ok(); + }); + app.MapPost("/login", () => Results.Json(new { jwt = "login-token" })); + app.MapPost("/oauth/token", (HttpContext c) => + ((string)c.Items["body"]!).Contains("grant_type=client_credentials") && + ((string)c.Items["body"]!).Contains("client_id=bitween") + ? Results.Json(new { access_token = "oauth-token", expires_in = 3600 }) + : Results.StatusCode(401)); + app.MapPost("/secure", (HttpContext c) => + c.Request.Headers.Authorization.ToString() is "Bearer login-token" or "Bearer oauth-token" + ? Results.Json(new { ok = true }) + : Results.StatusCode(401)); + app.MapGet("/shipments", (HttpContext c) => + c.Request.Headers["ApiKey"] == "partner-key" + ? Results.Json(new { data = new { items = new object[] { new { id = 1, carrier = "DHL" }, new { id = 2, carrier = "UPS" } } } }) + : Results.StatusCode(401)); + app.MapGet("/single", () => Results.Json(new { id = 7 })); + app.MapGet("/down", () => Results.StatusCode(503)); + + await app.StartAsync(); + Url = app.Services.GetRequiredService().Features.Get()!.Addresses.First(); + } + + public Received Last(string pathStart) => Requests.Last(r => r.Path.StartsWith(pathStart)); + + public async Task DisposeAsync() => await app.DisposeAsync(); +} + +/// The HTTP handler and receiver against a real HTTP server. +public class HttpTests(PartnerServer partner) : IClassFixture +{ + Dictionary Settings(string path, params (string Key, string Value)[] more) + { + var settings = new Dictionary { ["Url"] = partner.Url + path, ["TimeoutSeconds"] = "5" }; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + [Fact] + public async Task A_delivery_posts_the_message_to_the_url_its_values_fill_in_with_its_credentials_and_headers() + { + var handler = Adapters.Create(Settings("/orders/{{orderId}}", + ("AuthType", "Basic"), ("LoginUsername", "acme"), ("LoginPassword", "s3cret"), + ("ContentType", "application/json"), ("Headers", "X-Tenant:acme,X-Trace:abc-123"))); + + var response = await handler.Handle(new XchangeFile("{\"orderId\":\"SO-42\",\"lines\":3}")); + + Assert.False(response.BadData); + Assert.Equal("{\"accepted\":\"SO-42\"}", response.Data); + var received = partner.Last("/orders/SO-42"); + Assert.Equal("POST", received.Method); + Assert.Equal("{\"orderId\":\"SO-42\",\"lines\":3}", received.Body); + Assert.StartsWith("application/json", received.ContentType); + Assert.Equal("Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes("acme:s3cret")), received.Headers["Authorization"]); + Assert.Equal("acme", received.Headers["X-Tenant"]); + Assert.Equal("abc-123", received.Headers["X-Trace"]); + } + + [Theory] + [InlineData("X-Tenant: acme, X-Trace: abc-123")] + [InlineData("X-Tenant:acme,\nX-Trace:abc-123,")] + [InlineData("X-Tenant=acme\nX-Trace=abc-123")] + public async Task Headers_are_sent_however_the_list_is_spaced_or_separated(string headers) + { + var path = $"/orders/h{Guid.NewGuid():N}"; + await Adapters.Create(Settings(path, ("Headers", headers))) + .Handle(new XchangeFile("{}")); + + var received = partner.Last(path); + Assert.Equal("acme", received.Headers["X-Tenant"]); + Assert.Equal("abc-123", received.Headers["X-Trace"]); + } + + [Fact] + public async Task A_header_without_a_value_is_named_in_the_failure() + { + var failure = await Assert.ThrowsAsync(() => + Adapters.Create(Settings("/orders/x", ("Headers", "X-Tenant"))).Handle(new XchangeFile("{}"))); + Assert.Contains("'X-Tenant'", failure.Message); + } + + [Fact] + public async Task A_rejection_is_returned_as_a_bad_response_not_a_failure() + { + var response = await Adapters.Create(Settings("/bad")).Handle(new XchangeFile("{}")); + + Assert.True(response.BadData); + Assert.Contains("order has no lines", response.Data); + } + + [Theory] + [InlineData("/busy", "Retry-After: 30s")] + [InlineData("/busy", "429: TooManyRequests: ")] + [InlineData("/broken", "upstream exploded")] + // As the retry policy docs match it, and as it always read. + [InlineData("/broken", "502: BadGateway: ")] + public async Task Not_now_and_a_server_failure_fail_the_delivery_so_it_can_be_retried(string path, string expected) + { + var failure = await Assert.ThrowsAsync(() => + Adapters.Create(Settings(path)).Handle(new XchangeFile("{}"))); + Assert.Contains(expected, failure.Message); + } + + [Fact] + public async Task An_endpoint_that_does_not_answer_in_time_fails_the_delivery() + { + var failure = await Assert.ThrowsAsync(() => + Adapters.Create(Settings("/slow", ("TimeoutSeconds", "1"))).Handle(new XchangeFile("{}"))); + Assert.Contains("did not answer within 1 seconds", failure.Message); + } + + [Theory] + [InlineData("Login", "/login")] + [InlineData("OAuth2", "/oauth/token")] + public async Task A_token_is_fetched_from_the_sign_in_endpoint_and_sent_as_the_bearer(string authType, string loginPath) + { + var handler = Adapters.Create(Settings("/secure", + ("AuthType", authType), ("LoginUrl", partner.Url + loginPath), + ("LoginUsername", "acme"), ("LoginPassword", "s3cret"), + ("ClientId", "bitween"), ("ClientSecret", "oauth-secret"))); + + var response = await handler.Handle(new XchangeFile("{}")); + + Assert.False(response.BadData); + Assert.Equal("{\"ok\":true}", response.Data); + } + + [Fact] + public async Task The_receiver_pulls_the_list_with_its_key_and_hands_over_each_element() + { + var receiver = Adapters.Create(Settings("/shipments", + ("AuthType", "ApiKey"), ("ApiKey", "partner-key"), ("ArrayPath", "$.data.items"))); + await receiver.Initialize(); + + var ids = (await receiver.ListFiles()).ToList(); + + Assert.Equal(2, ids.Count); + var first = await receiver.GetFile(ids[0]); + Assert.Contains("\"carrier\": \"DHL\"", first.Data); + Assert.Contains("\"carrier\": \"UPS\"", (await receiver.GetFile(ids[1])).Data); + await receiver.DeleteFile(ids[0]); + Assert.Equal("GET", partner.Last("/shipments").Method); + } + + [Fact] + public async Task A_single_object_is_one_file_and_an_unavailable_source_fails_the_run() + { + var receiver = Adapters.Create(Settings("/single")); + Assert.Single(await receiver.ListFiles()); + + await Assert.ThrowsAnyAsync(() => Adapters.Create(Settings("/down")).ListFiles()); + await Assert.ThrowsAnyAsync(() => + Adapters.Create(Settings("/shipments", ("AuthType", "ApiKey"), ("ApiKey", "wrong"))).ListFiles()); + } +} diff --git a/SW.Bitween.TransportTests/OpenFtpTests.cs b/SW.Bitween.TransportTests/OpenFtpTests.cs new file mode 100644 index 00000000..75974288 --- /dev/null +++ b/SW.Bitween.TransportTests/OpenFtpTests.cs @@ -0,0 +1,425 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using SW.Bitween.NativeAdapters.FileTransfers; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.TransportTests; + +/// +/// The SFTP adapters, built on SSH.NET, against a real SFTP server. They need no license, so unlike +/// the Rebex adapters' tests these always run. +/// +public class OpenSftpTests(SftpFixture sftp) : IClassFixture +{ + Dictionary Settings(params (string Key, string Value)[] more) + { + var settings = new Dictionary + { + ["Host"] = sftp.Host, + ["Port"] = sftp.Port.ToString(), + ["Username"] = SftpFixture.User, + ["Password"] = SftpFixture.Password, + }; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + [Fact] + public async Task The_upload_handler_puts_the_file_in_the_target_folder_with_its_prefix() + { + await Adapters.Create(Settings(("TargetPath", "outbox"), ("FileNamePrefix", "acme"))) + .Handle(new XchangeFile("{\"order\":1}", "order-1.json")); + + Assert.Contains("acme_order-1.json", await sftp.ListAsync("outbox")); + Assert.Equal("{\"order\":1}", await sftp.ReadAsync("outbox/acme_order-1.json")); + } + + [Fact] + public async Task A_file_name_from_the_message_cannot_climb_out_of_the_target_folder() + { + await Adapters.Create(Settings(("TargetPath", "outbox"))) + .Handle(new XchangeFile("climbing", "../../inbox/escaped.txt")); + + Assert.Contains("escaped.txt", await sftp.ListAsync("outbox")); + Assert.DoesNotContain("escaped.txt", await sftp.ListAsync("inbox")); + } + + [Fact] + public async Task The_receiver_reads_the_folder_and_moves_each_file_once_received() + { + var name = $"r{Guid.NewGuid():N}.json"[..12]; + await sftp.PutAsync($"inbox/{name}", "{\"n\":1}"); + var receiver = Adapters.Create(Settings(("TargetPath", "inbox"), ("DeleteMovesFileTo", "../done"))); + await receiver.Initialize(); + + Assert.Contains(name, await receiver.ListFiles()); + Assert.Equal("{\"n\":1}", (await receiver.GetFile(name)).Data); + await receiver.DeleteFile(name); + // Already moved - a retried run - is skipped quietly. + await receiver.DeleteFile(name); + await receiver.Finalize(); + + Assert.DoesNotContain(name, await sftp.ListAsync("inbox")); + Assert.Contains(name, await sftp.ListAsync("done")); + } + + [Fact] + public async Task A_file_still_being_written_is_left_for_the_next_run() + { + var name = $"w{Guid.NewGuid():N}.json"[..12]; + await sftp.PutAsync($"inbox/{name}", "half"); + var receiver = Adapters.Create(Settings(("TargetPath", "inbox"), ("MinimumFileAgeSeconds", "300"))); + await receiver.Initialize(); + + Assert.DoesNotContain(name, await receiver.ListFiles()); + await receiver.Finalize(); + } + + [Fact] + public async Task A_PEM_key_pasted_onto_one_line_signs_in_and_the_pinned_host_key_is_accepted() + { + var receiver = Adapters.Create(Settings(("Password", ""), + ("PrivateKey", sftp.PrivateKey.ReplaceLineEndings(" ")), + ("HostKeyFingerprint", sftp.HostKeyFingerprint), ("TargetPath", "inbox"))); + + await receiver.Initialize(); + await receiver.ListFiles(); + await receiver.Finalize(); + } + + [Fact] + public async Task An_ed25519_key_in_the_OpenSSH_format_signs_in() + { + var receiver = Adapters.Create(Settings(("Password", ""), + ("PrivateKey", sftp.OpenSshPrivateKey), ("TargetPath", "inbox"))); + + await receiver.Initialize(); + await receiver.ListFiles(); + await receiver.Finalize(); + } + + [Fact] + public async Task A_server_whose_host_key_is_not_the_configured_one_gets_no_credentials() + { + var receiver = Adapters.Create(Settings(("TargetPath", "inbox"), + ("HostKeyFingerprint", "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))); + + var refused = await Assert.ThrowsAsync(() => receiver.Initialize()); + Assert.Contains("Refusing to send credentials", refused.Message); + } + + [Fact] + public async Task A_wrong_password_and_neither_password_nor_key_are_refused() + { + await Assert.ThrowsAnyAsync(() => + Adapters.Create(Settings(("Password", "wrong"))).Initialize()); + + var nothing = await Assert.ThrowsAsync(() => + Adapters.Create(Settings(("Password", ""))).Initialize()); + Assert.Contains("password or a private key", nothing.Message); + } +} + +/// +/// An SFTP server that signs in and then never answers: sshd runs a command that sleeps in place of +/// the SFTP server, so the session opens but the SFTP handshake gets no reply. What a broken partner +/// server did in the lab, where each upload waited until the server went away. +/// +public sealed class StallingSftpServer : IAsyncLifetime +{ + public IContainer Container { get; private set; } = null!; + + public async Task InitializeAsync() + { + Container = new ContainerBuilder() + .WithImage("atmoz/sftp:alpine") + .WithEntrypoint("sh", "-c", + "sed -i -e 's/^ForceCommand .*/ForceCommand sleep 3600/' -e '/^ChrootDirectory/d' /etc/ssh/sshd_config && " + + $"exec /entrypoint {SftpFixture.User}:{SftpFixture.Password}:1001:100:outbox") + .WithPortBinding(22, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(22)) + .Build(); + await Container.StartAsync(); + } + + public async Task DisposeAsync() + { + if (Container != null) await Container.DisposeAsync(); + } +} + +public class StallingSftpTests(StallingSftpServer server) : IClassFixture +{ + [Fact] + public async Task A_server_that_signs_in_but_never_opens_the_session_fails_the_upload_in_time() + { + var handler = Adapters.Create(new Dictionary + { + ["Host"] = server.Container.Hostname, + ["Port"] = server.Container.GetMappedPublicPort(22).ToString(), + ["Username"] = SftpFixture.User, + ["Password"] = SftpFixture.Password, + ["TargetPath"] = "outbox", + ["TimeoutSeconds"] = "3", + }); + + var clock = System.Diagnostics.Stopwatch.StartNew(); + var upload = handler.Handle(new XchangeFile("{}", "order-1.json")); + // Without a limit this never finishes; the wait here only stops the test hanging with it. + Assert.Same(upload, await Task.WhenAny(upload, Task.Delay(TimeSpan.FromSeconds(60)))); + await Assert.ThrowsAnyAsync(() => upload); + Assert.InRange(clock.Elapsed, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(30)); + } +} + +/// A real FTP server: plain, explicit FTPS or implicit FTPS, with a self-signed certificate. +public abstract class FtpServer : IAsyncLifetime +{ + public const string User = "partner"; + public const string Password = "partner-pass"; + + readonly string certificateDirectory = Path.Combine(Path.GetTempPath(), $"ftps-{Guid.NewGuid():N}"); + protected abstract string Mode { get; } + protected abstract int FirstPassivePort { get; } + int ControlPort => Mode == "implicit" ? 990 : 21; + + public IContainer Container { get; private set; } = null!; + public string Host => "127.0.0.1"; + public int Port => Container.GetMappedPublicPort(ControlPort); + + /// The server certificate's SHA-256 thumbprint, as a partner would hand it over. + public string CertificateThumbprint { get; private set; } = null!; + + public async Task InitializeAsync() + { + var builder = new ContainerBuilder() + .WithImage("delfer/alpine-ftp-server:latest") + .WithEnvironment("USERS", $"{User}|{Password}|/home/{User}|1001") + .WithPortBinding(ControlPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(ControlPort)); + // Passive mode tells the client which port to open next, so those ports are published as + // they are, not remapped. + for (var port = FirstPassivePort; port <= FirstPassivePort + 4; port++) + builder = builder.WithPortBinding(port, port); + + var passive = $"-opasv_min_port={FirstPassivePort} -opasv_max_port={FirstPassivePort + 4} -opasv_address=127.0.0.1"; + if (Mode == "none") + { + builder = builder + .WithEnvironment("ADDRESS", "127.0.0.1") + .WithEnvironment("MIN_PORT", FirstPassivePort.ToString()) + .WithEnvironment("MAX_PORT", (FirstPassivePort + 4).ToString()); + } + else + { + Directory.CreateDirectory(certificateDirectory); + using (var openssl = System.Diagnostics.Process.Start("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", Path.Combine(certificateDirectory, "key.pem"), "-out", Path.Combine(certificateDirectory, "cert.pem"), + "-days", "2", "-subj", "/CN=ftp.partner.test"])) + await openssl!.WaitForExitAsync(); + var certificate = X509Certificate2.CreateFromPem(await File.ReadAllTextAsync(Path.Combine(certificateDirectory, "cert.pem"))); + CertificateThumbprint = Convert.ToHexString(SHA256.HashData(certificate.RawData)); + + var tls = "-orsa_cert_file=/etc/ftps/cert.pem -orsa_private_key_file=/etc/ftps/key.pem -ossl_enable=YES " + + "-oforce_local_data_ssl=YES -oforce_local_logins_ssl=YES -ossl_tlsv1=NO -ossl_sslv2=NO -ossl_sslv3=NO " + + "-orequire_ssl_reuse=NO"; + var implicitTls = Mode == "implicit" ? "-oimplicit_ssl=YES -olisten_port=990" : ""; + builder = builder + .WithResourceMapping(new FileInfo(Path.Combine(certificateDirectory, "cert.pem")), "/etc/ftps/") + .WithResourceMapping(new FileInfo(Path.Combine(certificateDirectory, "key.pem")), "/etc/ftps/") + // The image creates the users, then runs this in place of its own vsftpd line. + // vsftpd forks into the background (its config says so), so the shell stays as the main process. + .WithCommand("sh", "-c", $"vsftpd {passive} {tls} {implicitTls} /etc/vsftpd/vsftpd.conf && exec sleep infinity"); + } + + Container = builder.Build(); + await Container.StartAsync(); + await Container.ExecAsync(["sh", "-c", $"mkdir -p /home/{User}/inbox /home/{User}/outbox /home/{User}/done && chown -R 1001 /home/{User}"]); + } + + public async Task ListAsync(string folder) => + (await Container.ExecAsync(["ls", "-1", $"/home/{User}/{folder}"])).Stdout; + + public async Task PutAsync(string path, string text) => + await Container.ExecAsync(["sh", "-c", $"printf '%s' '{text}' > /home/{User}/{path} && chown 1001 /home/{User}/{path}"]); + + public async Task ReadAsync(string path) => + (await Container.ExecAsync(["cat", $"/home/{User}/{path}"])).Stdout; + + public Dictionary Settings(params (string Key, string Value)[] more) + { + var settings = new Dictionary + { + ["Host"] = Host, + ["Port"] = Port.ToString(), + ["Username"] = User, + ["Password"] = Password, + ["Encryption"] = Mode, + }; + if (CertificateThumbprint != null) settings["CertificateThumbprint"] = CertificateThumbprint; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + public async Task DisposeAsync() + { + if (Container != null) await Container.DisposeAsync(); + try { Directory.Delete(certificateDirectory, true); } catch { } + } +} + +public sealed class PlainFtpServer : FtpServer +{ + protected override string Mode => "none"; + protected override int FirstPassivePort => 21100; +} + +public sealed class ExplicitFtpsServer : FtpServer +{ + protected override string Mode => "explicit"; + protected override int FirstPassivePort => 21120; +} + +public sealed class ImplicitFtpsServer : FtpServer +{ + protected override string Mode => "implicit"; + protected override int FirstPassivePort => 21140; +} + +/// The FTP adapters, built on FluentFTP, against a real plain FTP server. +public class OpenFtpTests(PlainFtpServer ftp) : IClassFixture +{ + const string Home = $"/home/{FtpServer.User}"; + + [Fact] + public async Task The_upload_handler_puts_the_file_in_the_target_folder_decoding_base64() + { + await Adapters.Create(ftp.Settings(("TargetPath", $"{Home}/outbox"), ("DataEncoding", "base64"))) + .Handle(new XchangeFile(Convert.ToBase64String("binary-ish"u8.ToArray()), "doc.bin")); + + Assert.Contains("doc.bin", await ftp.ListAsync("outbox")); + Assert.Equal("binary-ish", await ftp.ReadAsync("outbox/doc.bin")); + } + + [Fact] + public async Task The_receiver_lists_in_batches_reads_and_deletes_each_file() + { + for (var i = 0; i < 3; i++) await ftp.PutAsync($"inbox/f{i}.txt", $"file {i}"); + var receiver = Adapters.Create(ftp.Settings(("TargetPath", $"{Home}/inbox"), ("BatchSize", "2"))); + await receiver.Initialize(); + + var batch = (await receiver.ListFiles()).ToList(); + Assert.Equal(2, batch.Count); + foreach (var name in batch) + { + Assert.StartsWith("file ", (await receiver.GetFile(name)).Data); + await receiver.DeleteFile(name); + } + await receiver.Finalize(); + + Assert.Single((await ftp.ListAsync("inbox")).Split('\n', StringSplitOptions.RemoveEmptyEntries)); + } + + [Fact] + public async Task The_receiver_moves_a_received_file_when_told_where() + { + await ftp.PutAsync("inbox/move-me.txt", "moving"); + var receiver = Adapters.Create(ftp.Settings(("TargetPath", $"{Home}/inbox"), ("DeleteMovesFileTo", $"{Home}/done"))); + await receiver.Initialize(); + + await receiver.DeleteFile("move-me.txt"); + await receiver.Finalize(); + + Assert.Contains("move-me.txt", await ftp.ListAsync("done")); + Assert.DoesNotContain("move-me.txt", await ftp.ListAsync("inbox")); + } + + [Fact] + public async Task An_unknown_encryption_mode_is_named_in_the_failure() + { + var failure = await Assert.ThrowsAsync(() => + Adapters.Create(ftp.Settings(("Encryption", "ssl"))).Initialize()); + Assert.Contains("'ssl'", failure.Message); + } +} + +/// +/// FTPS with TLS negotiated on the FTP connection (AUTH TLS), against a real server that requires +/// it and holds a self-signed certificate - as many partners' servers do. +/// +public class ExplicitFtpsTests(ExplicitFtpsServer ftps) : IClassFixture +{ + const string Home = $"/home/{FtpServer.User}"; + + [Fact] + public async Task With_the_certificate_pinned_files_are_uploaded_and_picked_up_over_TLS() + { + await Adapters.Create(ftps.Settings(("TargetPath", $"{Home}/outbox"))) + .Handle(new XchangeFile("over tls", "secure.txt")); + Assert.Equal("over tls", await ftps.ReadAsync("outbox/secure.txt")); + + await ftps.PutAsync("inbox/in.txt", "incoming"); + var receiver = Adapters.Create(ftps.Settings(("TargetPath", $"{Home}/inbox"))); + await receiver.Initialize(); + Assert.Contains("in.txt", await receiver.ListFiles()); + Assert.Equal("incoming", (await receiver.GetFile("in.txt")).Data); + await receiver.DeleteFile("in.txt"); + await receiver.Finalize(); + Assert.DoesNotContain("in.txt", await ftps.ListAsync("inbox")); + } + + [Fact] + public async Task The_thumbprint_may_be_written_with_colons_and_in_lower_case() + { + var spaced = string.Join(":", ftps.CertificateThumbprint.Chunk(2).Select(c => new string(c))).ToLowerInvariant(); + var receiver = Adapters.Create(ftps.Settings(("CertificateThumbprint", spaced))); + + await receiver.Initialize(); + await receiver.Finalize(); + } + + [Fact] + public async Task A_self_signed_certificate_that_is_not_pinned_is_refused() + { + var receiver = Adapters.Create(ftps.Settings(("CertificateThumbprint", ""))); + await Assert.ThrowsAnyAsync(() => receiver.Initialize()); + } + + [Fact] + public async Task A_certificate_other_than_the_pinned_one_gets_no_credentials() + { + var receiver = Adapters.Create(ftps.Settings(("CertificateThumbprint", new string('A', 64)))); + + var refused = await Assert.ThrowsAsync(() => receiver.Initialize()); + Assert.Contains("Refusing to send credentials", refused.Message); + } + + [Fact] + public async Task Plain_FTP_is_refused_by_a_server_that_requires_TLS() + { + var receiver = Adapters.Create(ftps.Settings(("Encryption", "none"))); + await Assert.ThrowsAnyAsync(() => receiver.Initialize()); + } +} + +/// FTPS that is TLS from the first byte, on its own port, against a real server. +public class ImplicitFtpsTests(ImplicitFtpsServer ftps) : IClassFixture +{ + const string Home = $"/home/{FtpServer.User}"; + + [Fact] + public async Task With_the_certificate_pinned_a_file_is_uploaded_and_picked_up_over_TLS() + { + await Adapters.Create(ftps.Settings(("TargetPath", $"{Home}/outbox"))) + .Handle(new XchangeFile("implicit tls", "implicit.txt")); + Assert.Equal("implicit tls", await ftps.ReadAsync("outbox/implicit.txt")); + + var receiver = Adapters.Create(ftps.Settings(("TargetPath", $"{Home}/outbox"))); + await receiver.Initialize(); + Assert.Equal("implicit tls", (await receiver.GetFile("implicit.txt")).Data); + await receiver.Finalize(); + } +} diff --git a/SW.Bitween.TransportTests/S3Tests.cs b/SW.Bitween.TransportTests/S3Tests.cs new file mode 100644 index 00000000..00e65ac9 --- /dev/null +++ b/SW.Bitween.TransportTests/S3Tests.cs @@ -0,0 +1,188 @@ +using System.Text; +using Amazon.S3; +using Amazon.S3.Model; +using SW.Bitween.NativeAdapters.S3Receiver; +using SW.Bitween.NativeAdapters.S3UploadHandler; +using SW.PrimitiveTypes; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace SW.Bitween.TransportTests; + +/// +/// An S3-compatible server. SeaweedFS rather than MinIO, whose images are no longer published to a +/// public registry; with no identities configured it accepts any key pair, which is all the +/// adapters need to send. +/// +public sealed class S3Fixture : IAsyncLifetime +{ + readonly IContainer container = new ContainerBuilder() + .WithImage("chrislusf/seaweedfs:latest") + .WithResourceMapping(Encoding.UTF8.GetBytes( + """{"identities":[{"name":"tests","credentials":[{"accessKey":"transport-tests","secretKey":"transport-tests-secret"}],"actions":["Admin","Read","Write","List","Tagging"]}]}"""), + "/etc/seaweedfs/s3.json") + .WithCommand("server", "-s3", "-s3.config=/etc/seaweedfs/s3.json", "-dir=/data", + "-master.volumeSizeLimitMB=64", "-volume.max=200") + .WithPortBinding(8333, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8333).ForPath("/") + .ForStatusCodeMatching(code => (int)code < 500))) + .Build(); + + public AmazonS3Client Client { get; private set; } = null!; + public string Url => $"http://{container.Hostname}:{container.GetMappedPublicPort(8333)}"; + public string AccessKey => "transport-tests"; + public string SecretKey => "transport-tests-secret"; + + public async Task InitializeAsync() + { + await container.StartAsync(); + Client = new AmazonS3Client(AccessKey, SecretKey, new AmazonS3Config { ServiceURL = Url, ForcePathStyle = true }); + + // The S3 port answers before a volume is ready to take writes; one real write says it is. + var deadline = DateTime.UtcNow.AddSeconds(90); + while (true) + { + try + { + var bucket = await BucketAsync(); + await PutAsync(bucket, "ready", "ready"); + await Client.DeleteObjectAsync(bucket, "ready"); + return; + } + catch (AmazonS3Exception) when (DateTime.UtcNow < deadline) + { + await Task.Delay(1000); + } + } + } + + public async Task BucketAsync() + { + var bucket = $"b{Guid.NewGuid():N}"[..20]; + await Client.PutBucketAsync(bucket); + return bucket; + } + + public async Task PutAsync(string bucket, string key, string text) => + await Client.PutObjectAsync(new PutObjectRequest { BucketName = bucket, Key = key, ContentBody = text }); + + public async Task> KeysAsync(string bucket) => + (await Client.ListObjectsV2Async(new ListObjectsV2Request { BucketName = bucket })).S3Objects? + .Select(o => o.Key).OrderBy(k => k).ToList() ?? []; + + public async Task<(string Text, string ContentType)> GetAsync(string bucket, string key) + { + using var response = await Client.GetObjectAsync(bucket, key); + using var reader = new StreamReader(response.ResponseStream); + return (await reader.ReadToEndAsync(), response.Headers.ContentType); + } + + public async Task DisposeAsync() + { + Client?.Dispose(); + await container.DisposeAsync(); + } +} + +/// The S3 upload handler and receiver against a real S3-compatible server (SeaweedFS). +public class S3Tests(S3Fixture minio) : IClassFixture +{ + Dictionary Settings(string bucket, params (string Key, string Value)[] more) + { + var settings = new Dictionary + { + ["AccessKeyId"] = minio.AccessKey, + ["SecretAccessKey"] = minio.SecretKey, + ["ServiceUrl"] = minio.Url, + ["BucketName"] = bucket, + }; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + [Fact] + public async Task The_upload_handler_writes_the_file_under_its_folder_with_a_generated_name() + { + var bucket = await minio.BucketAsync(); + var handler = Adapters.Create(Settings(bucket, + ("FolderName", "outbox"), ("FileExtension", ".json"), ("ContentType", "application/json"))); + + var result = await handler.Handle(new XchangeFile("{\"order\":1}", "order.json")); + + Assert.Matches(@"^outbox/\d{14}_[0-9a-f]{32}\.json$", result.Data); + Assert.Equal([result.Data], await minio.KeysAsync(bucket)); + var (text, contentType) = await minio.GetAsync(bucket, result.Data); + Assert.Equal("{\"order\":1}", text); + Assert.Equal("application/json", contentType); + } + + /// + /// As shipped, a fixed file name is the whole key: the folder is not put in front of it. Pinned + /// so a change to it is a decision - deployments may be writing to the root on purpose. + /// + [Fact] + public async Task A_fixed_file_name_is_the_whole_key_and_overwrites() + { + var bucket = await minio.BucketAsync(); + var settings = Settings(bucket, ("FolderName", "outbox"), ("FileName", "daily/orders.csv")); + + await Adapters.Create(settings).Handle(new XchangeFile("first")); + await Adapters.Create(settings).Handle(new XchangeFile("second")); + + Assert.Equal(["daily/orders.csv"], await minio.KeysAsync(bucket)); + Assert.Equal("second", (await minio.GetAsync(bucket, "daily/orders.csv")).Text); + } + + [Fact] + public async Task The_receiver_lists_its_folder_in_batches_reads_each_file_and_moves_it_once_received() + { + var bucket = await minio.BucketAsync(); + await minio.PutAsync(bucket, "inbox/a.json", "{\"n\":\"a\"}"); + await minio.PutAsync(bucket, "inbox/sub/b.json", "{\"n\":\"b\"}"); + await minio.PutAsync(bucket, "inbox/c.json", "{\"n\":\"c\"}"); + await minio.PutAsync(bucket, "inbox/empty/", ""); + await minio.PutAsync(bucket, "elsewhere/x.json", "not mine"); + + var receiver = Adapters.Create(Settings(bucket, + ("FolderName", "inbox"), ("BatchSize", "2"), ("DeleteMovesFileTo", "done"))); + await receiver.Initialize(); + + var batch = (await receiver.ListFiles()).ToList(); + Assert.Equal(2, batch.Count); + Assert.All(batch, key => Assert.StartsWith("inbox/", key)); + + foreach (var key in batch) + { + var file = await receiver.GetFile(key); + Assert.StartsWith("{\"n\":", file.Data); + Assert.Equal(key, file.Filename); + await receiver.DeleteFile(key); + } + + var keys = await minio.KeysAsync(bucket); + Assert.All(batch, key => Assert.DoesNotContain(key, keys)); + Assert.All(batch, key => Assert.Contains($"done/{key["inbox/".Length..]}", keys)); + Assert.Contains("elsewhere/x.json", keys); + + // The rest comes in the next batch, the folder marker never does. + var next = (await receiver.ListFiles()).ToList(); + Assert.Single(next); + await receiver.Finalize(); + } + + [Fact] + public async Task The_receiver_deletes_a_received_file_when_there_is_nowhere_to_move_it() + { + var bucket = await minio.BucketAsync(); + await minio.PutAsync(bucket, "in/only.txt", "hello"); + var receiver = Adapters.Create(Settings(bucket, ("FolderName", "in"), ("ResponseEncoding", "base64"))); + await receiver.Initialize(); + + var key = Assert.Single(await receiver.ListFiles()); + Assert.Equal(Convert.ToBase64String(Encoding.UTF8.GetBytes("hello")), (await receiver.GetFile(key)).Data); + await receiver.DeleteFile(key); + + Assert.Empty(await minio.KeysAsync(bucket)); + } +} diff --git a/SW.Bitween.TransportTests/SW.Bitween.TransportTests.csproj b/SW.Bitween.TransportTests/SW.Bitween.TransportTests.csproj new file mode 100644 index 00000000..60a6b699 --- /dev/null +++ b/SW.Bitween.TransportTests/SW.Bitween.TransportTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/SW.Bitween.TransportTests/SftpTests.cs b/SW.Bitween.TransportTests/SftpTests.cs new file mode 100644 index 00000000..8cc27fa4 --- /dev/null +++ b/SW.Bitween.TransportTests/SftpTests.cs @@ -0,0 +1,143 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using SW.Bitween.NativeAdapters.RebexFtpReceiver; +using SW.Bitween.NativeAdapters.RebexFtpUploadHandler; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.TransportTests; + +/// Runs only with a Rebex license key in REBEX_LICENSE_KEY: the FTP adapters are built on Rebex. +public sealed class RebexFactAttribute : FactAttribute +{ + public RebexFactAttribute() + { + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("REBEX_LICENSE_KEY"))) + Skip = "Set REBEX_LICENSE_KEY to run the FTP/SFTP adapters, which are built on Rebex."; + } +} + +public sealed class SftpFixture : IAsyncLifetime +{ + public const string User = "partner"; + public const string Password = "partner-pass"; + + readonly string keyDirectory = Path.Combine(Path.GetTempPath(), $"sftp-key-{Guid.NewGuid():N}"); + + /// A private key the server accepts for , PEM-encoded as most partners hand them over. + public string PrivateKey { get; private set; } = null!; + + /// An ed25519 key in the OpenSSH format ssh-keygen writes by default, also accepted. + public string OpenSshPrivateKey { get; private set; } = null!; + + /// The server's ed25519 host key, as ssh-keygen -lf prints it. + public string HostKeyFingerprint { get; private set; } = null!; + + // An "inbox", an "outbox" and a "done" in the user's home, which is all a partner's server usually offers. + public IContainer Container { get; private set; } = null!; + + public string Host => Container.Hostname; + public int Port => Container.GetMappedPublicPort(22); + + public async Task InitializeAsync() + { + Directory.CreateDirectory(keyDirectory); + var keyPath = Path.Combine(keyDirectory, "id_rsa"); + using (var keygen = System.Diagnostics.Process.Start("ssh-keygen", ["-t", "rsa", "-b", "2048", "-m", "PEM", "-N", "", "-q", "-f", keyPath])) + await keygen!.WaitForExitAsync(); + PrivateKey = await File.ReadAllTextAsync(keyPath); + var openSshPath = Path.Combine(keyDirectory, "id_ed25519"); + using (var keygen = System.Diagnostics.Process.Start("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", openSshPath])) + await keygen!.WaitForExitAsync(); + OpenSshPrivateKey = await File.ReadAllTextAsync(openSshPath); + + Container = new ContainerBuilder() + .WithImage("atmoz/sftp:alpine") + .WithCommand($"{User}:{Password}:1001:100:inbox,outbox,done") + .WithResourceMapping(new FileInfo(keyPath + ".pub"), $"/home/{User}/.ssh/keys/") + .WithResourceMapping(new FileInfo(openSshPath + ".pub"), $"/home/{User}/.ssh/keys/") + .WithPortBinding(22, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(22)) + .Build(); + await Container.StartAsync(); + + var printed = (await Container.ExecAsync(["ssh-keygen", "-lf", "/etc/ssh/ssh_host_ed25519_key.pub"])).Stdout; + HostKeyFingerprint = printed.Split(' ')[1]; + } + + public async Task ListAsync(string folder) => + (await Container.ExecAsync(["ls", "-1", $"/home/{User}/{folder}"])).Stdout; + + public async Task PutAsync(string path, string text) => + await Container.ExecAsync(["sh", "-c", $"printf '%s' '{text}' > /home/{User}/{path} && chown 1001 /home/{User}/{path}"]); + + public async Task ReadAsync(string path) => + (await Container.ExecAsync(["cat", $"/home/{User}/{path}"])).Stdout; + + public async Task DisposeAsync() + { + if (Container != null) await Container.DisposeAsync(); + try { Directory.Delete(keyDirectory, true); } catch { } + } +} + +/// The FTP adapters over SFTP against a real SFTP server. +public class SftpTests(SftpFixture sftp) : IClassFixture +{ + static string LicenseKey => Environment.GetEnvironmentVariable("REBEX_LICENSE_KEY")!; + + Dictionary Settings(params (string Key, string Value)[] more) + { + var settings = new Dictionary + { + ["Host"] = sftp.Host, + ["Port"] = sftp.Port.ToString(), + ["Username"] = SftpFixture.User, + ["Password"] = SftpFixture.Password, + ["Protocol"] = "sftp", + }; + foreach (var (key, value) in more) settings[key] = value; + return settings; + } + + [RebexFact] + public async Task The_upload_handler_puts_the_file_in_the_target_folder_with_its_prefix() + { + var handler = new NativeRebexFtpUploadHandler(LicenseKey); + handler.InitializeStartupValues(Settings(("TargetPath", "outbox"), ("FileNamePrefix", "acme"))); + + await handler.Handle(new XchangeFile("{\"order\":1}", "order-1.json")); + + var name = Assert.Single((await sftp.ListAsync("outbox")).Split('\n', StringSplitOptions.RemoveEmptyEntries), + n => n.StartsWith("acme_")); + Assert.Equal("{\"order\":1}", await sftp.ReadAsync($"outbox/{name}")); + } + + [RebexFact] + public async Task The_receiver_reads_the_inbox_and_moves_each_file_once_received() + { + await sftp.PutAsync("inbox/a.json", "{\"n\":1}"); + var receiver = new NativeRebexFtpReceiver(LicenseKey); + receiver.InitializeStartupValues(Settings(("TargetPath", "inbox"), ("DeleteMovesFileTo", "done"))); + await receiver.Initialize(); + + var file = Assert.Single(await receiver.ListFiles()); + Assert.Equal("{\"n\":1}", (await receiver.GetFile(file)).Data); + await receiver.DeleteFile(file); + await receiver.Finalize(); + + Assert.DoesNotContain("a.json", await sftp.ListAsync("inbox")); + Assert.Contains("a.json", await sftp.ListAsync("done")); + } + + [RebexFact] + public async Task A_server_whose_host_key_is_not_the_configured_one_gets_no_credentials() + { + var receiver = new NativeRebexFtpReceiver(LicenseKey); + receiver.InitializeStartupValues(Settings(("TargetPath", "inbox"), + ("HostKeyFingerprint", "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))); + + var refused = await Assert.ThrowsAsync(() => receiver.Initialize()); + Assert.Contains("Refusing to send credentials", refused.Message); + } +} diff --git a/SW.Bitween.UnitTests/AccountCreateValidatorTests.cs b/SW.Bitween.UnitTests/AccountCreateValidatorTests.cs index d0960c9e..488d88d4 100644 --- a/SW.Bitween.UnitTests/AccountCreateValidatorTests.cs +++ b/SW.Bitween.UnitTests/AccountCreateValidatorTests.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.UnitTests; /// /// /// These validators are nested private classes discovered by the request pipeline, so a test that -/// calls the handler directly never runs them — which is exactly how the handler and its validator +/// calls the handler directly never runs them - which is exactly how the handler and its validator /// came to disagree. The handler was updated to take explicit role ids and treat the coarse /// Role as legacy, but the validator went on demanding Role unconditionally, so the /// payload the UI actually sends was rejected and adding a member failed outright. @@ -20,7 +20,7 @@ namespace SW.Bitween.UnitTests; public class AccountCreateValidatorTests { /// - /// Builds the handler's nested validator. Reflection because it is private by design — the + /// Builds the handler's nested validator. Reflection because it is private by design - the /// pipeline finds it by scanning, and nothing else is meant to construct one. /// private static IValidator Validator(bool disableEmailPasswordLogin = false) @@ -65,7 +65,7 @@ public void The_legacy_coarse_role_alone_is_still_accepted() public void Naming_no_role_at_all_is_refused() { // The rule the original NotNull was really protecting. When Role was a plain int, omitting - // it sent 0 — which is Admin — so a member added with nothing ticked got the run of the + // it sent 0 - which is Admin - so a member added with nothing ticked got the run of the // instance. Neither field given has to stay a refusal. CollectionAssert.Contains(FailedFields(Valid()), nameof(CreateAccountModel.Role)); } @@ -76,7 +76,7 @@ public void An_empty_role_id_list_counts_as_naming_no_role() var model = Valid(); model.RoleIds = []; - // An empty list is not an answer — it is the absence of one, and must not slip past the + // An empty list is not an answer - it is the absence of one, and must not slip past the // guard just because the property was present. CollectionAssert.Contains(FailedFields(model), nameof(CreateAccountModel.Role)); } diff --git a/SW.Bitween.UnitTests/AdapterContractTests.cs b/SW.Bitween.UnitTests/AdapterContractTests.cs new file mode 100644 index 00000000..dc240fea --- /dev/null +++ b/SW.Bitween.UnitTests/AdapterContractTests.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NJsonSchema; +using SW.Bitween.Adapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +/// +/// The Bitween adapter contract, and the two .NET families that implement it: adapters built on +/// SW.PrimitiveTypes (IInfolinkHandler and friends) and on SW.Bitween.Adapters (IBitweenHandler and +/// friends). Bitween calls both by method name and reads their JSON into SW.PrimitiveTypes' types, +/// so the method names must be the contract's and the JSON must be identical - and JSON written by +/// an adapter in any other language must read the same way. +/// +[TestClass] +public class AdapterContractTests +{ + static JObject Contract => JObject.Parse(AdapterContract.Json); + + static string Json(object value) => JsonConvert.SerializeObject(value); + + [TestMethod] + public void An_exchange_file_is_the_same_JSON_whichever_family_wrote_it() + { + var old = new XchangeFile("{\"order\":1}", "order.json", badData: true) { ContentType = "application/json" }; + var current = new ExchangeFile("{\"order\":1}", "order.json", badData: true) { ContentType = "application/json" }; + + Assert.IsTrue(JToken.DeepEquals(JToken.Parse(Json(old)), JToken.Parse(Json(current))), + $"old: {Json(old)}\nnew: {Json(current)}"); + } + + [TestMethod] + public void An_exchange_file_reads_back_into_either_family() + { + var fromNew = JsonConvert.DeserializeObject(Json(new ExchangeFile("payload", "a.txt", true)))!; + Assert.AreEqual("payload", fromNew.Data); + Assert.AreEqual("a.txt", fromNew.Filename); + Assert.IsTrue(fromNew.BadData); + + var fromOld = JsonConvert.DeserializeObject(Json(new XchangeFile("payload", "a.txt", true)))!; + Assert.AreEqual("payload", fromOld.Data); + Assert.AreEqual("a.txt", fromOld.Filename); + Assert.IsTrue(fromOld.BadData); + Assert.AreEqual(new XchangeFile("payload").Hash, fromOld.Hash, "the hash is computed the same way"); + } + + /// What a Python, Node or Go adapter writes: no Hash, nulls, fields Bitween doesn't know. + [DataTestMethod] + [DataRow("{\"Data\":\"x\"}", "x", null, false)] + [DataRow("{\"Data\":\"x\",\"Filename\":null,\"BadData\":false,\"ContentType\":null}", "x", null, false)] + [DataRow("{\"Data\":\"rejected\",\"Filename\":\"r.json\",\"BadData\":true,\"Unknown\":{\"a\":1}}", "rejected", "r.json", true)] + [DataRow("{\"Data\":\"x\",\"Hash\":\"not-the-real-hash\"}", "x", null, false)] + public void Another_language_s_exchange_file_is_read_by_Bitween_as_it_means(string json, string data, string filename, bool badData) + { + var read = JsonConvert.DeserializeObject(json)!; + + Assert.AreEqual(data, read.Data); + Assert.AreEqual(filename, read.Filename); + Assert.AreEqual(badData, read.BadData); + Assert.AreEqual(new XchangeFile(data).Hash, read.Hash, "a hash sent in is recomputed, never trusted"); + } + + [TestMethod] + public void A_validation_result_is_the_same_JSON_whichever_family_wrote_it() + { + var old = new InfolinkValidatorResult(); + old.AddError("Id", "Must be under 10"); + old.AddError("Name", "Required"); + var current = new ValidationResult().AddError("Id", "Must be under 10").AddError("Name", "Required"); + + Assert.IsTrue(JToken.DeepEquals(JToken.Parse(Json(old)), JToken.Parse(Json(current))), + $"old: {Json(old)}\nnew: {Json(current)}"); + Assert.IsTrue(JToken.DeepEquals(JToken.Parse(Json(new InfolinkValidatorResult())), JToken.Parse(Json(new ValidationResult())))); + } + + [TestMethod] + public void A_validation_result_reads_back_into_either_family() + { + var fromNew = JsonConvert.DeserializeObject(Json(new ValidationResult().AddError("Id", "Bad")))!; + Assert.IsFalse(fromNew.Success); + Assert.AreEqual("Id", fromNew.Validations.Single().Key); + + var old = new InfolinkValidatorResult(); + old.AddError("Id", "Bad"); + var fromOld = JsonConvert.DeserializeObject(Json(old))!; + Assert.IsFalse(fromOld.Success); + Assert.IsTrue(fromOld.Has("Id")); + + // Another language may leave Success out; Bitween derives it from the failures. + Assert.IsTrue(JsonConvert.DeserializeObject("{\"Validations\":[]}")!.Success); + Assert.IsFalse(JsonConvert.DeserializeObject( + "{\"Validations\":[{\"Key\":\"Id\",\"Value\":\"Bad\"}]}")!.Success); + } + + [TestMethod] + public async Task What_both_families_write_satisfies_the_published_schemas() + { + var fileSchema = await JsonSchema.FromJsonAsync(AdapterContract.ExchangeFileSchema); + var resultSchema = await JsonSchema.FromJsonAsync(AdapterContract.ValidationResultSchema); + var old = new InfolinkValidatorResult(); + old.AddError("Id", "Bad"); + + foreach (var json in new[] { Json(new XchangeFile("x", "f")), Json(new ExchangeFile("x", "f")), "{\"Data\":\"x\"}" }) + Assert.AreEqual(0, fileSchema.Validate(json).Count, json); + foreach (var json in new[] { Json(old), Json(new ValidationResult().AddError("Id", "Bad")), "{\"Validations\":[]}" }) + Assert.AreEqual(0, resultSchema.Validate(json).Count, json); + + Assert.AreNotEqual(0, fileSchema.Validate("{\"Filename\":\"no data\"}").Count, "a file without Data is not a file"); + Assert.AreNotEqual(0, resultSchema.Validate("{\"Validations\":[{\"Key\":\"Id\"}]}").Count, "a failure needs its message"); + } + + /// The example inputs serverless test calls adapters with are themselves what the contract says. + [TestMethod] + public async Task Every_example_input_satisfies_its_method_s_input_schema() + { + var schemas = new Dictionary + { + ["ExchangeFile"] = await JsonSchema.FromJsonAsync(AdapterContract.ExchangeFileSchema), + ["ValidationResult"] = await JsonSchema.FromJsonAsync(AdapterContract.ValidationResultSchema), + }; + var checkedExamples = 0; + foreach (var kind in Contract["kinds"]!.Children()) + foreach (var method in kind.Value["methods"]!) + foreach (var example in method["examples"] ?? new JArray()) + { + var input = (string)method["input"]!; + Assert.IsTrue(schemas.ContainsKey(input), $"{kind.Name}.{method["name"]} has an example for {input}, which has no schema"); + var errors = schemas[input].Validate(example.ToString()); + Assert.AreEqual(0, errors.Count, $"{kind.Name}.{method["name"]}: {string.Join("; ", errors)}"); + // And Bitween reads it as the file it is. + Assert.IsFalse(string.IsNullOrEmpty(example.ToObject()!.Data)); + checkedExamples++; + } + Assert.IsTrue(checkedExamples >= 3, "handler, mapper and validator each have an example"); + } + + /// Bitween calls by method name: each kind's names in the contract are the methods both families declare. + [DataTestMethod] + [DataRow("handler", typeof(IBitweenHandler), typeof(IInfolinkHandler))] + [DataRow("mapper", typeof(IBitweenMapper), typeof(IInfolinkHandler))] + [DataRow("validator", typeof(IBitweenValidator), typeof(IInfolinkValidator))] + [DataRow("receiver", typeof(IBitweenReceiver), typeof(IInfolinkReceiver))] + public void Each_kind_s_methods_are_the_contract_s_in_both_families(string kind, System.Type current, System.Type old) + { + var names = Contract["kinds"]![kind]!["methods"]!.Select(m => (string)m["name"]!).OrderBy(n => n).ToList(); + + CollectionAssert.AreEqual(names, current.GetMethods().Select(m => m.Name).OrderBy(n => n).ToList(), current.Name); + CollectionAssert.AreEqual(names, old.GetMethods().Select(m => m.Name).OrderBy(n => n).ToList(), old.Name); + + foreach (var method in Contract["kinds"]![kind]!["methods"]!) + { + var name = (string)method["name"]!; + Assert.AreEqual(Shape(current.GetMethod(name)!), Shape(old.GetMethod(name)!), $"{kind}.{name} differs between the families"); + Assert.AreEqual(ContractShape(method), Shape(current.GetMethod(name)!), $"{kind}.{name} differs from the contract"); + } + } + + /// A method's argument and result, in the contract's words. + static string Shape(MethodInfo method) + { + static string Of(System.Type? type) => type switch + { + null => "none", + _ when type == typeof(Task) => "none", + _ when type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>) => Of(type.GetGenericArguments()[0]), + _ when type == typeof(XchangeFile) || type == typeof(ExchangeFile) => "ExchangeFile", + _ when type == typeof(InfolinkValidatorResult) || type == typeof(ValidationResult) => "ValidationResult", + _ when type == typeof(string) => "FileId", + _ when type == typeof(IEnumerable) => "FileIdList", + _ => type.Name, + }; + return $"{Of(method.GetParameters().SingleOrDefault()?.ParameterType)} -> {Of(method.ReturnType)}"; + } + + static string ContractShape(JToken method) => + $"{(string?)method["input"] ?? "none"} -> {(string?)method["output"] ?? "none"}"; +} diff --git a/SW.Bitween.UnitTests/AdapterFailureReaderTests.cs b/SW.Bitween.UnitTests/AdapterFailureReaderTests.cs index 2a04b3fd..5097adee 100644 --- a/SW.Bitween.UnitTests/AdapterFailureReaderTests.cs +++ b/SW.Bitween.UnitTests/AdapterFailureReaderTests.cs @@ -84,8 +84,8 @@ public void The_last_error_wins_over_ordinary_chatter() [TestMethod] public void An_adapter_that_never_used_the_logger_still_says_something() { - // Dying outside the SDK's logger is allowed — an unhandled exception on a background - // thread prints a bare stack trace — and half an answer beats none. + // Dying outside the SDK's logger is allowed - an unhandled exception on a background + // thread prints a bare stack trace - and half an answer beats none. var summary = AdapterFailureReader.Summarise(["Unhandled exception. System.IO.IOException: pipe broken"]); StringAssert.Contains(summary, "IOException"); @@ -116,7 +116,7 @@ public void Explain_leads_with_the_adapters_reason() [TestMethod] public void Explain_falls_back_to_what_bitween_saw() { - // No output at all — the process died before printing, or was killed outright. + // No output at all - the process died before printing, or was killed outright. Assert.AreEqual("Adapter stream closed.", AdapterFailureReader.Explain("Adapter stream closed.", null)); } diff --git a/SW.Bitween.UnitTests/AdapterInvokerTests.cs b/SW.Bitween.UnitTests/AdapterInvokerTests.cs index 52bc839c..2ef04d87 100644 --- a/SW.Bitween.UnitTests/AdapterInvokerTests.cs +++ b/SW.Bitween.UnitTests/AdapterInvokerTests.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.UnitTests; /// /// Which runtime runs an adapter. /// -/// There are three — in-process, a spawned process, a pooled long-lived one — and they are not +/// There are three - in-process, a spawned process, a pooled long-lived one - and they are not /// interchangeable: sending a resident adapter down the classic path does not fail cleanly, it /// waits out the command timeout for an answer that will never come. Choosing correctly is /// therefore not a detail, and it is made here rather than at every call site so there is one @@ -59,7 +59,7 @@ public async Task The_catch_all_runtime_only_sees_what_the_others_declined() /// /// The role travels with the call. A mapper and a handler are both invoked as Handle, and only - /// the role tells a native runtime which of the two registrations to resolve — so losing it + /// the role tells a native runtime which of the two registrations to resolve - so losing it /// here would silently run the wrong adapter. /// [TestMethod] @@ -88,7 +88,7 @@ public async Task Properties_and_the_correlation_id_reach_the_runtime() /// /// A one-call invoke has to release the session. For a resident adapter the session IS a - /// pooled lease, so leaking one takes a warm instance out of circulation for good — and the + /// pooled lease, so leaking one takes a warm instance out of circulation for good - and the /// pool runs dry with no error to say why. /// [TestMethod] diff --git a/SW.Bitween.UnitTests/ApiTests.cs b/SW.Bitween.UnitTests/ApiTests.cs deleted file mode 100644 index c1626702..00000000 --- a/SW.Bitween.UnitTests/ApiTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using SW.Bitween.Model; -using SW.PrimitiveTypes; - -namespace SW.Bitween.UnitTests -{ - [TestClass] - public class ApiTests - { - - static TestServer server; - - [ClassInitialize] - public static void ClassInitialize(TestContext tcontext) - { - server = new TestServer(WebHost.CreateDefaultBuilder() - .UseEnvironment("Development") - .UseStartup()); - } - - [ClassCleanup] - public static void ClassCleanup() - { - server.Dispose(); - } - - [TestMethod] - public Task TestSubmitXchangeFail() - { - return Task.CompletedTask; - //var xs = new XchangeRequest - //{ - // SubscriberId = 100000, - - //}; - - //var body = JsonConvert.SerializeObject(xs);//, _jsonSettings); - //using var client = server.CreateClient(); - //var response = await client.PostAsync("/cqapi/xchanges", new StringContent(body, Encoding.UTF8, "application/json")); - //var responseBody = await response.Content.ReadAsStringAsync(); - - //Assert.AreEqual(response.StatusCode, HttpStatusCode.BadRequest); - - } - - [TestMethod] - public Task TestSubmitXchangeSuccess() - { - return Task.CompletedTask; - //var xs = new XchangeRequest - //{ - // SubscriberId = 1, - // File = new XchangeFile("test") - - //}; - - //var body = JsonConvert.SerializeObject(xs);//, _jsonSettings); - //using var client = server.CreateClient(); - //var response = await client.PostAsync("/cqapi/xchanges", new StringContent(body, Encoding.UTF8, "application/json")); - //var responseBody = await response.Content.ReadAsStringAsync(); - //response.EnsureSuccessStatusCode(); - - ////var response2 = await client.GetAsync($"/api/xchange/{responseBody}"); - ////var responseBody2 = await response2.Content.ReadAsStringAsync(); - ////response2.EnsureSuccessStatusCode(); - - } - - [TestMethod] - public Task TestDocFilterFail() - { - //var xs = new XchangeRequest - //{ - // DocumentId = 1, - // File = new XchangeFile( "{data: 1}") - //}; - - //var body = JsonConvert.SerializeObject(xs); - //using var client = server.CreateClient(); - - //var response = await client.PostAsync("/cqapi/xchanges", new StringContent(body, Encoding.UTF8, "application/json")); - //var responseBody = await response.Content.ReadAsStringAsync(); - - //Assert.AreEqual(response.StatusCode, HttpStatusCode.BadRequest); - return Task.CompletedTask; - } - - //[TestMethod] - //public async Task TestGetAccessKeySet() - //{ - - // AccessKeySetConfig accessKeySetConfig = new AccessKeySetConfig { Key1="1",Key2="2" ,Name="test"}; - // var body = JsonConvert.SerializeObject(accessKeySetConfig); - // using var client = server.CreateClient(); - - // await client.PostAsync("/cqapi/accesskeysets", new StringContent(body, Encoding.UTF8, "application/json")); - - // var response = await client.GetAsync("/cqapi/accesskeysets/1"); - // var responseBody = await response.Content.ReadAsStringAsync(); - - // Assert.AreEqual(response.StatusCode, HttpStatusCode.OK); - - // //var getaccessks = serviceProvider.GetService>(); - - // //var result = await getaccessks.Get("1"); - - // //var getaccessks1 = serviceProvider.GetService>(); - // //var result1 = await getaccessks1.LookupList(null, new PrimitiveTypes.SearchyRequest("Id", PrimitiveTypes.SearchyRule.EqualsTo, 1)); - - // //var getadapter = serviceProvider.GetService>(); - - // //var result2 = await getadapter.Get("1"); - - // //var updateadapter = serviceProvider.GetService>(); - - // //result2.Description = "test desc"; - // ////result2.Properties.Add("ss", "sss"); - - // //await updateadapter.Update("1", result2); - - // //var getreceiver = serviceProvider.GetService>(); - - // //var result3 = await getreceiver.Get("1"); - - // //var updateReceiver = serviceProvider.GetService>(); - // //result3.Schedules.Remove(result3.Schedules.First()); - // //result3.Schedules.Add(new Schedule(Recurrence.Monthly, TimeSpan.FromMinutes(10))); - - //} - - [TestMethod] - public void TestValueTypes() - { - var file = new XchangeFile("test data", "file.txt"); - - var str = JsonConvert.SerializeObject(file); - - var filed = JsonConvert.DeserializeObject(str); - } - - - } -} diff --git a/SW.Bitween.UnitTests/BusServiceTests.cs b/SW.Bitween.UnitTests/BusServiceTests.cs deleted file mode 100644 index 5a04d129..00000000 --- a/SW.Bitween.UnitTests/BusServiceTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace SW.Bitween.UnitTests -{ - [TestClass] - public class BusServiceTests - { - static TestServer server; - - [ClassInitialize] - public static void ClassInitialize(TestContext tcontext) - { - server = new TestServer(WebHost.CreateDefaultBuilder() - .UseEnvironment("Development") - .UseStartup()); - } - - [ClassCleanup] - public static void ClassCleanup() - { - server.Dispose(); - } - - [TestMethod] - public Task TestGetMessageTypeNameToDocumentIdMap() - { - - //var constr = server.Host.Services.GetRequiredService().GetConnectionString("RabbitMQ"); - //var memCache = server.Host.Services.GetRequiredService(); - //var result = await memCache.GetMessageTypeNameToDocumentIdMap(); - //result = await memCache.GetMessageTypeNameToDocumentIdMap(); - return Task.CompletedTask; - } - } -} diff --git a/SW.Bitween.UnitTests/CapabilityVersionTests.cs b/SW.Bitween.UnitTests/CapabilityVersionTests.cs index 8d44f8a6..96f5c033 100644 --- a/SW.Bitween.UnitTests/CapabilityVersionTests.cs +++ b/SW.Bitween.UnitTests/CapabilityVersionTests.cs @@ -7,7 +7,7 @@ namespace SW.Bitween.UnitTests; /// /// A capability list is written per engine and is therefore about the engine at its NEWEST. Where /// a feature arrived in a specific release, the list has to be narrowed once the server version is -/// known — otherwise it promises something an older server will refuse, and the promise is found +/// known - otherwise it promises something an older server will refuse, and the promise is found /// out on the first message rather than on the screen that made it. /// [TestClass] diff --git a/SW.Bitween.UnitTests/ConfigurationFileTests.cs b/SW.Bitween.UnitTests/ConfigurationFileTests.cs new file mode 100644 index 00000000..9911999c --- /dev/null +++ b/SW.Bitween.UnitTests/ConfigurationFileTests.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using SW.Bitween.Cli.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +/// +/// The pieces of bitween apply that don't need a Bitween: reading and writing YAML so a written file +/// reads back the same, substituting secrets and files, and the rules for when two values are the same. +/// +[TestClass] +public class ConfigurationFileTests +{ + readonly string folder = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "bitween-config-tests", Guid.NewGuid().ToString("N"))).FullName; + + [TestCleanup] + public void Cleanup() + { + try { Directory.Delete(folder, true); } catch { } + } + + [TestMethod] + public void Yaml_written_reads_back_as_the_same_values() + { + var original = new JsonObject + { + ["name"] = "Orders: inbound # one", + ["count"] = 3, + ["ratio"] = 0.5, + ["on"] = true, + ["code"] = "007", + ["looksTrue"] = "true", + ["empty"] = "", + ["nothing"] = null, + ["sql"] = "select *\nfrom orders\nwhere id > :cursor\n", + ["dash"] = "- not a list", + ["nested"] = new JsonObject { ["list"] = new JsonArray(1, "two", new JsonObject { ["a"] = "b", ["c"] = new JsonArray("d") }) }, + ["lists"] = new JsonArray(new JsonArray("x", "y"), new JsonArray()), + ["needs quotes"] = "${secret:X}", + }; + + var back = Yaml.Read(Yaml.Write(original)).Single(); + + Assert.AreEqual(original.ToJsonString(), back!.ToJsonString()); + } + + [TestMethod] + public void Secrets_variables_and_files_are_substituted_and_secrets_remembered() + { + File.WriteAllText(Path.Combine(folder, "schema.json"), "{ \"type\": \"object\" }"); + var secrets = Directory.CreateDirectory(Path.Combine(folder, "secrets")).FullName; + File.WriteAllText(Path.Combine(secrets, "FROM_FOLDER"), "folder-secret\n"); + Environment.SetEnvironmentVariable("CONFIG_TEST_SECRET", "env-secret"); + Environment.SetEnvironmentVariable("CONFIG_TEST_REGION", "eu"); + try + { + File.WriteAllText(Path.Combine(folder, "a.yaml"), """ + apiVersion: bitween/v1 + kind: Configuration + metadata: + name: test + informationTypes: + - name: Order + validationSchema: ${file:schema.json} + """); + File.WriteAllText(Path.Combine(folder, "b.yml"), """ + apiVersion: bitween/v1 + kind: Configuration + partners: + - name: Acme ${env:CONFIG_TEST_REGION} + keys: + main: ${secret:CONFIG_TEST_SECRET} + other: ${secret:FROM_FOLDER} + """); + + var config = ConfigFile.Load(folder, secrets); + + Assert.AreEqual("test", config.Name); + Assert.AreEqual("{ \"type\": \"object\" }", (string)config.Sections["informationTypes"][0]["validationSchema"]!); + var partner = config.Sections["partners"][0]; + Assert.AreEqual("Acme eu", (string)partner["name"]!); + Assert.AreEqual("env-secret", (string)partner["keys"]!["main"]!); + Assert.AreEqual("folder-secret", (string)partner["keys"]!["other"]!); + CollectionAssert.AreEqual(new[] { "env-secret", "folder-secret" }, config.SecretValues.Order().ToArray()); + } + finally + { + Environment.SetEnvironmentVariable("CONFIG_TEST_SECRET", null); + Environment.SetEnvironmentVariable("CONFIG_TEST_REGION", null); + } + } + + [TestMethod] + public void A_file_with_missing_secrets_or_unknown_sections_is_refused_naming_each() + { + var file = Path.Combine(folder, "c.yaml"); + File.WriteAllText(file, """ + apiVersion: bitween/v1 + kind: Configuration + metadata: { name: x } + partners: + - name: A + keys: { main: "${secret:NOT_SET_ONE}", other: "${secret:NOT_SET_TWO}" } + """); + var missing = Assert.ThrowsException(() => ConfigFile.Load(file)); + StringAssert.Contains(missing.Message, "NOT_SET_ONE, NOT_SET_TWO"); + + File.WriteAllText(file, "apiVersion: bitween/v1\nkind: Configuration\nmetadata: { name: x }\nwidgets: []\n"); + StringAssert.Contains(Assert.ThrowsException(() => ConfigFile.Load(file)).Message, "no section widgets"); + + File.WriteAllText(file, "apiVersion: bitween/v2\nkind: Configuration\n"); + StringAssert.Contains(Assert.ThrowsException(() => ConfigFile.Load(file)).Message, "apiVersion must be bitween/v1"); + } + + [TestMethod] + public void Values_compare_by_what_the_file_says() + { + // Only what the file sets is compared; Bitween's defaults for the rest don't count. + Assert.IsTrue(Values.Same(JsonNode.Parse("""{ "a": 1 }"""), JsonNode.Parse("""{ "a": 1, "b": 2 }"""))); + // ...except a mapping that replaces every key, which has to name them all. + Assert.IsFalse(Values.Same(JsonNode.Parse("""{ "a": "1" }"""), JsonNode.Parse("""{ "a": "1", "b": "2" }"""), exactKeys: true)); + // A secret Bitween won't show matches anything. + Assert.IsTrue(Values.Same(JsonValue.Create("s3cret"), JsonValue.Create(Values.Masked))); + // Empty is empty, however it's said. + Assert.IsTrue(Values.Same(null, new JsonArray())); + Assert.IsTrue(Values.Same(JsonValue.Create(""), null)); + // A number is a number, however it's written. + Assert.IsTrue(Values.Same(JsonValue.Create(3), JsonValue.Create("3"))); + Assert.IsTrue(Values.Same(JsonValue.Create(1.0), JsonValue.Create(1))); + Assert.IsFalse(Values.Same(JsonValue.Create("a"), JsonValue.Create("A"))); + Assert.IsFalse(Values.Same(JsonNode.Parse("[1, 2]"), JsonNode.Parse("[2, 1]"))); + } + + [TestMethod] + public void Secret_names_are_made_from_what_they_belong_to() + { + Assert.AreEqual("PARTNER_ACME_LTD_KEY_MAIN", Exporter.Name(["partner", "Acme Ltd.", "key", "main"])); + Assert.AreEqual("SUBSCRIPTION_ORDERS_TO_WAREHOUSE_HANDLER_APIKEY", Exporter.Name(["subscription", "Orders to warehouse!", "handler", "ApiKey"])); + } + + [TestMethod] + public void An_aggregation_with_an_information_type_is_turned_away_before_anything_is_made() + { + var aggregation = new JsonObject { ["name"] = "Hourly manifest", ["type"] = "Aggregation", ["aggregationFor"] = "Returns", ["informationType"] = "Customer return" }; + var problems = new Subscriptions().Unresolved(aggregation, (_, _) => true).ToList(); + Assert.IsTrue(problems.Any(p => p.Contains("an aggregation has none of its own")), string.Join("; ", problems)); + + aggregation.Remove("informationType"); + Assert.AreEqual(0, new Subscriptions().Unresolved(aggregation, (_, _) => true).Count()); + // Any other kind keeps its information type. + var receiving = new JsonObject { ["name"] = "Pickup", ["type"] = "Receiving", ["informationType"] = "Customer return" }; + Assert.AreEqual(0, new Subscriptions().Unresolved(receiving, (_, _) => true).Count()); + } + + [TestMethod] + public void A_retry_budget_without_a_shared_total_is_turned_away_since_it_would_never_retry() + { + JsonObject Policy(JsonObject budget) => new() + { + ["name"] = "Partner outage", + ["groups"] = new JsonArray(new JsonObject { ["name"] = "down or slow", ["appliesTo"] = new JsonArray("Error"), ["budget"] = budget }), + }; + var missing = new RetryPolicies().Unresolved(Policy(new JsonObject { ["maxAttemptsPerError"] = 6 }), (_, _) => true).ToList(); + Assert.IsTrue(missing.Any(p => p.Contains("no retries at all")), string.Join("; ", missing)); + + Assert.AreEqual(0, new RetryPolicies().Unresolved(Policy(new JsonObject { ["maxAttemptsPerError"] = 6, ["maxAttemptsTotal"] = 500 }), (_, _) => true).Count()); + // Null is no ceiling, and 0 written out is taken as meant. + Assert.AreEqual(0, new RetryPolicies().Unresolved(Policy(new JsonObject { ["maxAttemptsPerError"] = 6, ["maxAttemptsTotal"] = null }), (_, _) => true).Count()); + Assert.AreEqual(0, new RetryPolicies().Unresolved(Policy(new JsonObject { ["maxAttemptsPerError"] = 6, ["maxAttemptsTotal"] = 0 }), (_, _) => true).Count()); + } + + /// + /// Bitween answers expectTraffic with every member, nulls included. Exported, it names only what + /// is set; compared, a member either side leaves unset is unset on both, and one set on only one + /// side is a change, so a plan neither repeats itself nor misses a removed time zone. + /// + [TestMethod] + public void Expected_traffic_compares_by_what_is_set() + { + static JsonObject Api(string zone) => new() + { + ["name"] = "Orders", ["type"] = "GatewayApiCall", ["pausedOn"] = null, + ["expectTraffic"] = new JsonObject + { + ["everyMinutes"] = 15, ["days"] = null, ["from"] = null, ["to"] = null, ["timeZone"] = zone, + }, + }; + var ctx = new Context(null); + var kind = new Subscriptions(); + var have = kind.ToExisting(ctx, "1", Api(null)); + + Assert.AreEqual("""{"everyMinutes":15}""", have.File["expectTraffic"]!.ToJsonString()); + + JsonObject Want(JsonNode expect) => new() { ["name"] = "Orders", ["expectTraffic"] = expect }; + Assert.IsFalse(kind.Differences(ctx, Want(new JsonObject { ["everyMinutes"] = 15 }), have).Contains("expectTraffic")); + Assert.IsFalse(kind.Differences(ctx, Want(new JsonObject { ["everyMinutes"] = "15", ["days"] = new JsonArray() }), have) + .Contains("expectTraffic")); + Assert.IsTrue(kind.Differences(ctx, Want(new JsonObject { ["everyMinutes"] = 30 }), have).Contains("expectTraffic")); + Assert.IsTrue(kind.Differences(ctx, Want(null), have).Contains("expectTraffic")); + + var zoned = kind.ToExisting(ctx, "1", Api("Asia/Riyadh")); + Assert.IsTrue(kind.Differences(ctx, Want(new JsonObject { ["everyMinutes"] = 15 }), zoned).Contains("expectTraffic")); + + var none = kind.ToExisting(ctx, "1", new JsonObject { ["name"] = "Orders", ["expectTraffic"] = null }); + Assert.IsFalse(kind.Differences(ctx, Want(null), none).Contains("expectTraffic")); + } +} diff --git a/SW.Bitween.UnitTests/DestinationCircuitsTests.cs b/SW.Bitween.UnitTests/DestinationCircuitsTests.cs new file mode 100644 index 00000000..ebf4f8f1 --- /dev/null +++ b/SW.Bitween.UnitTests/DestinationCircuitsTests.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.FileTransfers; +using SW.Bitween.NativeAdapters.SmtpHandler; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class DestinationCircuitsTests +{ + const string Erp = "https://erp.example"; + + sealed class Clock : TimeProvider + { + public DateTimeOffset Now = new(2026, 10, 12, 10, 0, 0, TimeSpan.Zero); + public override DateTimeOffset GetUtcNow() => Now; + public void Advance(int seconds) => Now = Now.AddSeconds(seconds); + } + + static (DestinationCircuits Circuits, Clock Clock) Breaker(int failures = 3, int window = 60, int open = 60) + { + var clock = new Clock(); + return (new DestinationCircuits(new CircuitBreakerSettings(failures, window, open), clock), clock); + } + + static void Fail(DestinationCircuits circuits, string destination = Erp) + { + using var pass = circuits.Enter(destination); + pass.Failed(); + } + + static void Succeed(DestinationCircuits circuits, string destination = Erp) + { + using var pass = circuits.Enter(destination); + pass.Succeeded(); + } + + [TestMethod] + public void Failures_in_a_row_open_the_circuit_and_calls_then_fail_at_once_as_a_503() + { + var (circuits, _) = Breaker(); + Fail(circuits); + Fail(circuits); + Succeed(circuits); + Fail(circuits); + Fail(circuits); + circuits.Enter(Erp).Dispose(); + Fail(circuits); + + var ex = Assert.ThrowsException(() => circuits.Enter(Erp)); + + Assert.AreEqual("503: ServiceUnavailable: circuit open for https://erp.example (3 failures in a row; trying again after 10:01:00Z)", ex.Message); + Assert.AreEqual(Erp, ex.Destination); + } + + [TestMethod] + public void A_success_between_failures_starts_the_count_again() + { + var (circuits, _) = Breaker(); + for (var i = 0; i < 10; i++) + { + Fail(circuits); + Fail(circuits); + Succeed(circuits); + } + circuits.Enter(Erp).Dispose(); + } + + [TestMethod] + public void Failures_spread_wider_than_the_window_do_not_open_it() + { + var (circuits, clock) = Breaker(window: 60); + for (var i = 0; i < 10; i++) + { + Fail(circuits); + clock.Advance(31); + } + circuits.Enter(Erp).Dispose(); + + Fail(circuits); + clock.Advance(1); + Fail(circuits); + Assert.ThrowsException(() => circuits.Enter(Erp)); + } + + [TestMethod] + public void Each_destination_has_its_own_circuit() + { + var (circuits, _) = Breaker(); + for (var i = 0; i < 3; i++) Fail(circuits); + + Assert.ThrowsException(() => circuits.Enter(Erp)); + circuits.Enter("https://warehouse.example").Dispose(); + circuits.Enter("https://erp.example:8443").Dispose(); + } + + [TestMethod] + public void After_the_cool_down_one_trial_goes_through_and_its_success_closes_the_circuit() + { + var (circuits, clock) = Breaker(open: 60); + for (var i = 0; i < 3; i++) Fail(circuits); + + clock.Advance(59); + Assert.ThrowsException(() => circuits.Enter(Erp)); + clock.Advance(1); + + var trial = circuits.Enter(Erp); + var ex = Assert.ThrowsException(() => circuits.Enter(Erp), "one trial at a time"); + StringAssert.Contains(ex.Message, "a trial call is under way"); + + trial.Succeeded(); + circuits.Enter(Erp).Dispose(); + Succeed(circuits); + // Closed means counting from nothing again. + Fail(circuits); + Fail(circuits); + circuits.Enter(Erp).Dispose(); + } + + [TestMethod] + public void A_failed_trial_opens_the_circuit_for_another_cool_down() + { + var (circuits, clock) = Breaker(open: 60); + for (var i = 0; i < 3; i++) Fail(circuits); + clock.Advance(60); + + Fail(circuits); + + var ex = Assert.ThrowsException(() => circuits.Enter(Erp)); + StringAssert.Contains(ex.Message, "the trial call failed; trying again after 10:02:00Z"); + clock.Advance(60); + Succeed(circuits); + circuits.Enter(Erp).Dispose(); + } + + [TestMethod] + public void A_trial_that_says_nothing_about_the_destination_lets_the_next_call_try() + { + var (circuits, clock) = Breaker(); + for (var i = 0; i < 3; i++) Fail(circuits); + clock.Advance(60); + + using (var trial = circuits.Enter(Erp)) + trial.Failed(new SWException("The login response has no token at 'jwt'.")); + + var next = circuits.Enter(Erp); + Assert.ThrowsException(() => circuits.Enter(Erp)); + next.Succeeded(); + } + + [TestMethod] + public void A_trial_that_never_reports_back_does_not_keep_the_circuit_shut() + { + var (circuits, clock) = Breaker(open: 60); + for (var i = 0; i < 3; i++) Fail(circuits); + clock.Advance(60); + _ = circuits.Enter(Erp); + + clock.Advance(59); + Assert.ThrowsException(() => circuits.Enter(Erp)); + clock.Advance(1); + circuits.Enter(Erp).Succeeded(); + } + + [TestMethod] + public void Late_failures_from_calls_let_in_before_it_opened_do_not_extend_it() + { + var (circuits, clock) = Breaker(open: 60); + var late = circuits.Enter(Erp); + for (var i = 0; i < 3; i++) Fail(circuits); + clock.Advance(30); + late.Failed(); + + clock.Advance(30); + circuits.Enter(Erp).Succeeded(); + } + + [TestMethod] + public void Zero_failures_switches_it_off() + { + var (circuits, _) = Breaker(failures: 0); + for (var i = 0; i < 100; i++) Fail(circuits); + circuits.Enter(Erp).Dispose(); + Assert.IsFalse(DestinationCircuits.None.Enabled); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.TooManyRequests, true)] + [DataRow(HttpStatusCode.InternalServerError, true)] + [DataRow(HttpStatusCode.BadGateway, true)] + [DataRow(HttpStatusCode.ServiceUnavailable, true)] + [DataRow(HttpStatusCode.GatewayTimeout, true)] + [DataRow(HttpStatusCode.NotImplemented, false)] + [DataRow(HttpStatusCode.BadRequest, false)] + [DataRow(HttpStatusCode.Unauthorized, false)] + [DataRow(HttpStatusCode.NotFound, false)] + [DataRow(HttpStatusCode.Conflict, false)] + [DataRow(HttpStatusCode.UnprocessableEntity, false)] + [DataRow(HttpStatusCode.RequestTimeout, false)] + [DataRow(HttpStatusCode.OK, false)] + public void Which_statuses_count(HttpStatusCode status, bool counts) => + Assert.AreEqual(counts, DestinationCircuits.IsUnwell(status)); + + [TestMethod] + public void Which_failures_count() + { + Assert.IsTrue(DestinationCircuits.IsUnwell(new TimeoutException())); + Assert.IsTrue(DestinationCircuits.IsUnwell(new TaskCanceledException())); + Assert.IsTrue(DestinationCircuits.IsUnwell(new HttpRequestException(HttpRequestError.ConnectionError, "refused", + new SocketException((int)SocketError.ConnectionRefused)))); + Assert.IsTrue(DestinationCircuits.IsUnwell(new HttpRequestException("reset", new IOException("connection reset")))); + Assert.IsTrue(DestinationCircuits.IsUnwell(new Renci.SshNet.Common.SshOperationTimeoutException())); + Assert.IsTrue(DestinationCircuits.IsUnwell(new Renci.SshNet.Common.SshConnectionException())); + Assert.IsTrue(DestinationCircuits.IsUnwell(new MailKit.Net.Smtp.SmtpCommandException( + MailKit.Net.Smtp.SmtpErrorCode.UnexpectedStatusCode, MailKit.Net.Smtp.SmtpStatusCode.ServiceNotAvailable, "421 busy"))); + + Assert.IsFalse(DestinationCircuits.IsUnwell(new SWException("The login response is not JSON"))); + Assert.IsFalse(DestinationCircuits.IsUnwell(new ArgumentException("Unknown DataEncoding"))); + Assert.IsFalse(DestinationCircuits.IsUnwell(new FileNotFoundException())); + Assert.IsFalse(DestinationCircuits.IsUnwell(new Renci.SshNet.Common.SshAuthenticationException())); + Assert.IsFalse(DestinationCircuits.IsUnwell(new MailKit.Net.Smtp.SmtpCommandException( + MailKit.Net.Smtp.SmtpErrorCode.RecipientNotAccepted, MailKit.Net.Smtp.SmtpStatusCode.MailboxUnavailable, "550 no such user"))); + Assert.IsFalse(DestinationCircuits.IsUnwell(new CircuitOpenException(Erp, "503: open"))); + } + + [TestMethod] + public async Task The_http_handler_stops_calling_a_failing_destination_and_keeps_calling_a_healthy_one() + { + var (circuits, clock) = Breaker(); + var calls = new List(); + var server = new Scripted(request => + { + calls.Add(request.RequestUri!.Host); + return new HttpResponseMessage(request.RequestUri.Host == "erp.example" + ? HttpStatusCode.ServiceUnavailable + : HttpStatusCode.OK) { Content = new StringContent("{}") }; + }); + + for (var i = 0; i < 3; i++) + await Assert.ThrowsExceptionAsync(() => Http(server, circuits, "https://erp.example/orders").Handle(new XchangeFile("{}"))); + + var ex = await Assert.ThrowsExceptionAsync(() => + Http(server, circuits, "https://erp.example/invoices").Handle(new XchangeFile("{}"))); + StringAssert.StartsWith(ex.Message, "503: ServiceUnavailable: circuit open for https://erp.example ("); + Assert.AreEqual(3, calls.Count, "the open circuit sent nothing"); + + await Http(server, circuits, "https://warehouse.example/orders").Handle(new XchangeFile("{}")); + Assert.AreEqual(4, calls.Count); + + clock.Advance(60); + await Assert.ThrowsExceptionAsync(() => Http(server, circuits, "https://erp.example/orders").Handle(new XchangeFile("{}"))); + Assert.AreEqual(5, calls.Count, "the trial went through"); + } + + [TestMethod] + public async Task Business_rejections_never_open_the_http_handlers_circuit() + { + var (circuits, _) = Breaker(); + var server = new Scripted(_ => new HttpResponseMessage(HttpStatusCode.UnprocessableEntity) { Content = new StringContent("no") }); + + for (var i = 0; i < 10; i++) + Assert.IsTrue((await Http(server, circuits, "https://erp.example/orders").Handle(new XchangeFile("{}"))).BadData); + Assert.AreEqual(10, server.Requests.Count); + } + + [TestMethod] + public void The_other_handlers_take_the_registry_or_go_without() + { + // Constructed bare, as tests and older callers do, they run without a breaker. + _ = new NativeSmtpHandler(); + _ = new NativeSftpUploadHandler(); + _ = new NativeFtpUploadHandler(Breaker().Circuits); + } + + [TestMethod] + public async Task A_file_upload_to_an_unreachable_server_counts_and_then_fails_at_once() + { + var (circuits, _) = Breaker(failures: 2); + // Bound and never listened on, so connecting is refused straight away. + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + var port = ((IPEndPoint)socket.LocalEndPoint!).Port; + + NativeFtpUploadHandler Upload() + { + var handler = new NativeFtpUploadHandler(circuits); + handler.InitializeStartupValues(new Dictionary + { + ["Host"] = "127.0.0.1", ["Port"] = port.ToString(), ["Username"] = "u", ["Password"] = "p", + }); + return handler; + } + + for (var i = 0; i < 2; i++) + { + try + { + await Upload().Handle(new XchangeFile("x")); + Assert.Fail("nothing listens there"); + } + catch (Exception e) when (e is not CircuitOpenException and not AssertFailedException) { } + } + + var ex = await Assert.ThrowsExceptionAsync(() => Upload().Handle(new XchangeFile("x"))); + StringAssert.StartsWith(ex.Message, $"503: ServiceUnavailable: circuit open for ftp://127.0.0.1:{port} ("); + } + + static NativeHttpHandler Http(Scripted server, DestinationCircuits circuits, string url) + { + var handler = new NativeHttpHandler(new Proxy(new HttpClient(server)), circuits); + handler.InitializeStartupValues(new Dictionary { ["Url"] = url }); + return handler; + } + + sealed class Proxy(HttpClient client) : IDynamicHttpProxy + { + public HttpClient GetClient(string fullUrl) => client; + } + + sealed class Scripted(Func answer) : HttpMessageHandler + { + public List Requests { get; } = new(); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Requests.Add(request); + return Task.FromResult(answer(request)); + } + } +} diff --git a/SW.Bitween.UnitTests/DocumentSchemaTests.cs b/SW.Bitween.UnitTests/DocumentSchemaTests.cs new file mode 100644 index 00000000..e2ae4eb3 --- /dev/null +++ b/SW.Bitween.UnitTests/DocumentSchemaTests.cs @@ -0,0 +1,118 @@ +using System.Linq; +using System.Threading.Tasks; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +/// Checking documents against the schema an information type carries. +[TestClass] +public class DocumentSchemaTests +{ + private const string OrderSchema = """ + { + "type": "object", + "required": ["orderId", "lines"], + "additionalProperties": false, + "properties": { + "orderId": { "type": "string" }, + "lines": { "type": "array", "items": { "$ref": "#/definitions/line" } } + }, + "definitions": { + "line": { "type": "object", "required": ["sku"], "properties": { "sku": { "type": "string" } } } + } + } + """; + + private const string OrderXsd = """ + + + + + + + + + + + """; + + private static async Task Clean(Task> check) + { + var errors = await check; + Assert.AreEqual(0, errors.Count, string.Join("\n", errors)); + } + + [TestMethod] + public async Task A_matching_json_document_has_no_errors() + { + await Clean(DocumentSchema.Check(DocumentFormat.Json, OrderSchema, + """{ "orderId": "A1", "lines": [ { "sku": "X" } ] }""")); + } + + [TestMethod] + public async Task A_json_document_that_doesnt_match_says_where() + { + var errors = await DocumentSchema.Check(DocumentFormat.Json, OrderSchema, + """{ "lines": [ { "qty": 1 } ], "extra": true }"""); + + Assert.IsTrue(errors.Any(e => e.Contains("orderId"))); + Assert.IsTrue(errors.Any(e => e.Contains("sku"))); + Assert.IsTrue(errors.Any(e => e.Contains("extra"))); + } + + [TestMethod] + public async Task What_the_older_partner_endpoint_adds_is_not_held_against_the_document() + { + await Clean(DocumentSchema.Check(DocumentFormat.Json, OrderSchema, + """{ "orderId": "A1", "lines": [], "_ExternalRequestContext": "[]" }""")); + } + + [TestMethod] + public async Task Malformed_json_is_one_error_not_an_exception() + { + var errors = await DocumentSchema.Check(DocumentFormat.Json, OrderSchema, "{ not json"); + Assert.AreEqual(1, errors.Count); + Assert.IsTrue(errors[0].StartsWith("Not valid JSON")); + } + + [TestMethod] + public async Task An_xml_document_is_checked_against_its_xsd() + { + await Clean(DocumentSchema.Check(DocumentFormat.Xml, OrderXsd, + "A12")); + + var errors = await DocumentSchema.Check(DocumentFormat.Xml, OrderXsd, + "A1two"); + Assert.IsTrue(errors.Any(e => e.Contains("quantity"))); + + Assert.IsTrue((await DocumentSchema.Check(DocumentFormat.Xml, OrderXsd, ""))[0].StartsWith("Not valid XML")); + } + + [TestMethod] + public async Task No_schema_and_carried_formats_check_nothing() + { + await Clean(DocumentSchema.Check(DocumentFormat.Json, null, "{ not json")); + await Clean(DocumentSchema.Check(DocumentFormat.Csv, OrderSchema, "a,b")); + } + + [DataTestMethod] + [DataRow(DocumentFormat.Json, "{ \"type\": ")] + [DataRow(DocumentFormat.Json, "{ \"$ref\": \"https://example.com/order.json\" }")] + [DataRow(DocumentFormat.Json, "{ \"$ref\": \"order.json\" }")] + [DataRow(DocumentFormat.Xml, "")] + [DataRow(DocumentFormat.Xml, "")] + [DataRow(DocumentFormat.Csv, "{}")] + public async Task A_schema_that_cant_be_used_or_reaches_outside_itself_is_refused(DocumentFormat format, string schema) + { + await Assert.ThrowsExceptionAsync(() => DocumentSchema.EnsureUsable(format, schema)); + } + + [TestMethod] + public async Task A_schema_fixed_after_being_refused_is_accepted() + { + await Assert.ThrowsExceptionAsync(() => DocumentSchema.EnsureUsable(DocumentFormat.Json, "{")); + await DocumentSchema.EnsureUsable(DocumentFormat.Json, OrderSchema); + } +} diff --git a/SW.Bitween.UnitTests/FakePop3Server.cs b/SW.Bitween.UnitTests/FakePop3Server.cs index 57bf2434..51f8d949 100644 --- a/SW.Bitween.UnitTests/FakePop3Server.cs +++ b/SW.Bitween.UnitTests/FakePop3Server.cs @@ -23,12 +23,19 @@ public sealed class FakePop3Server : IDisposable private readonly string _expectedPassword; private readonly CancellationTokenSource _cts = new(); private readonly Task _acceptTask; + private readonly System.Security.Cryptography.X509Certificates.X509Certificate2? _tls; + + /// Whether anyone got as far as sending a password. + public bool PasswordSent { get; private set; } public int Port { get; } public IReadOnlyCollection DeletedMessageNumbers => _deleted; - public FakePop3Server(IEnumerable messages, string expectedUser = "user", string expectedPassword = "pass") + /// With a certificate, TLS from the first byte, as on port 995. + public FakePop3Server(IEnumerable messages, string expectedUser = "user", string expectedPassword = "pass", + System.Security.Cryptography.X509Certificates.X509Certificate2? tls = null) { + _tls = tls; _messages = messages.ToList(); _expectedUser = expectedUser; _expectedPassword = expectedPassword; @@ -61,7 +68,15 @@ private async Task AcceptLoop(CancellationToken ct) private async Task HandleClient(TcpClient client, CancellationToken ct) { using var _ = client; - await using var stream = client.GetStream(); + await using var network = client.GetStream(); + Stream stream = network; + if (_tls != null) + { + var ssl = new System.Net.Security.SslStream(network, leaveInnerStreamOpen: true); + try { await ssl.AuthenticateAsServerAsync(_tls); } + catch { return; } + stream = ssl; + } var reader = new StreamReader(stream, Encoding.ASCII, false, 1024, leaveOpen: true); var writer = new StreamWriter(stream, Encoding.ASCII, 1024, leaveOpen: true) { NewLine = "\r\n", AutoFlush = true }; @@ -90,6 +105,7 @@ private async Task HandleClient(TcpClient client, CancellationToken ct) break; case "PASS": + PasswordSent = true; if (authenticatedUser == _expectedUser && argument == _expectedPassword) await writer.WriteLineAsync("+OK logged in"); else diff --git a/SW.Bitween.UnitTests/FileUploadNamingTests.cs b/SW.Bitween.UnitTests/FileUploadNamingTests.cs new file mode 100644 index 00000000..729ff362 --- /dev/null +++ b/SW.Bitween.UnitTests/FileUploadNamingTests.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters.FileTransfers; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +/// +/// What the FTP and SFTP upload handlers call the file. Every message through one gateway carries the +/// same name, so without a name of its own each upload replaced the last; FileName gives each its own. +/// +[TestClass] +public class FileUploadNamingTests +{ + static readonly DateTime Now = new(2026, 10, 11, 18, 30, 5, 123, DateTimeKind.Utc); + + static string Name(string data, string? filename, string? fileName = null, string? prefix = null, string encoding = "utf8") => + FileUpload.NameFor(new XchangeFile(data, filename), + new SftpUploadSettings { FileName = fileName, FileNamePrefix = prefix, DataEncoding = encoding }, Now); + + const string Order = """{"labId":"f3-1","order":{"id":"MKT-42"}}"""; + + [TestMethod] + public void Without_a_file_name_setting_nothing_changes() + { + Assert.AreEqual("acme-bulk-orders.json", Name(Order, "acme-bulk-orders.json")); + Assert.AreEqual("bulk_acme-bulk-orders.json", Name(Order, "acme-bulk-orders.json", prefix: "bulk")); + Assert.AreEqual("20261011183005123", Name(Order, null)); + } + + [TestMethod] + public void A_file_name_setting_is_filled_from_the_message_so_each_message_gets_its_own_file() + { + Assert.AreEqual("order-f3-1.json", Name(Order, "acme-bulk-orders.json", "order-{{ labId }}.json")); + Assert.AreEqual("bulk_MKT-42.json", Name(Order, "acme-bulk-orders.json", "{{ order.id }}.json", prefix: "bulk")); + Assert.AreNotEqual( + Name(Order, "acme-bulk-orders.json", "order-{{ labId }}.json"), + Name(Order.Replace("f3-1", "f3-2"), "acme-bulk-orders.json", "order-{{ labId }}.json")); + } + + [TestMethod] + public void A_name_that_comes_out_empty_or_reaches_outside_the_folder_is_made_safe() + { + // A field the message doesn't have leaves nothing, and the message's own name is used. + Assert.AreEqual("acme-bulk-orders.json", Name(Order, "acme-bulk-orders.json", "{{ missing }}")); + // Only the last segment of a path is kept, so a template can't write elsewhere. + Assert.AreEqual("passwd", Name("""{"x":"../../etc/passwd"}""", "a.json", "{{ x }}")); + } + + [TestMethod] + public void A_message_that_isnt_JSON_takes_the_setting_as_written() + { + Assert.AreEqual("daily.csv", Name("a,b\n1,2", "upload.csv", "daily.csv")); + Assert.AreEqual("export-{{ id }}.bin", Name("AQID", "x.bin", "export-{{ id }}.bin", encoding: "base64")); + } +} diff --git a/SW.Bitween.UnitTests/FtpProtocolTests.cs b/SW.Bitween.UnitTests/FtpProtocolTests.cs new file mode 100644 index 00000000..da91cc49 --- /dev/null +++ b/SW.Bitween.UnitTests/FtpProtocolTests.cs @@ -0,0 +1,29 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class FtpProtocolTests +{ + /// OpenSSH prints "SHA256:" and drops the padding; either form must match the other. + [DataTestMethod] + [DataRow("SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8", "nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8=")] + [DataRow(" sha256:abc= ", "abc")] + public void Fingerprints_compare_across_formats(string configured, string presented) => + Assert.AreEqual(FtpProtocol.NormalizeFingerprint(configured), FtpProtocol.NormalizeFingerprint(presented)); + + /// + /// The upload name comes from the exchange - from an email subject, for a POP3 receiver - so + /// it is cut to one name and can't climb out of the target directory. + /// + [DataTestMethod] + [DataRow("invoice.xml", "invoice.xml")] + [DataRow("../../etc/cron.d/x", "x")] + [DataRow("..\\..\\windows\\x.bat", "x.bat")] + [DataRow("/absolute/path/report.csv", "report.csv")] + [DataRow("..", null)] + [DataRow("dir/", null)] + public void Upload_names_are_one_file_name(string given, string expected) => + Assert.AreEqual(expected, FtpProtocol.SafeFileName(given)); +} diff --git a/SW.Bitween.UnitTests/GatewayFailureAnswerTests.cs b/SW.Bitween.UnitTests/GatewayFailureAnswerTests.cs new file mode 100644 index 00000000..d3ee14f0 --- /dev/null +++ b/SW.Bitween.UnitTests/GatewayFailureAnswerTests.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using SW.Bitween.Controllers; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +/// +/// What a sync gateway call is told when the delivery behind it failed: 400 for gateways that answer as +/// they always did, otherwise what Bitween will do next. +/// +[TestClass] +public class GatewayFailureAnswerTests +{ + static readonly DateTime Next = new(2026, 10, 12, 8, 20, 0, DateTimeKind.Utc); + + static JObject Body(GatewayFailureAnswers.Answer answer) => JObject.FromObject(answer.Body); + + [TestMethod] + public void A_legacy_gateway_answers_400_with_nothing_whatever_happened() + { + foreach (var retryOn in new DateTime?[] { null, Next }) + { + var answer = GatewayFailureAnswers.For(GatewayFailureAnswer.Legacy, "x1", hasMapper: true, mapped: true, retryOn); + Assert.AreEqual(400, answer.Status); + Assert.IsNull(answer.Body); + } + } + + [TestMethod] + public void A_failure_Bitween_will_retry_is_accepted_so_the_caller_does_not_send_it_again() + { + var answer = GatewayFailureAnswers.For(GatewayFailureAnswer.Accurate, "x1", hasMapper: true, mapped: true, Next); + Assert.AreEqual(202, answer.Status); + Assert.AreEqual("x1", (string)Body(answer)["exchangeId"]); + Assert.AreEqual("retrying", (string)Body(answer)["status"]); + Assert.AreEqual(Next, (DateTime)Body(answer)["nextAttemptOn"]); + } + + [TestMethod] + public void A_delivery_that_failed_for_good_is_a_bad_gateway_and_a_message_that_could_not_be_mapped_is_unprocessable() + { + var delivery = GatewayFailureAnswers.For(GatewayFailureAnswer.Accurate, "x2", hasMapper: true, mapped: true, null); + Assert.AreEqual(502, delivery.Status); + Assert.AreEqual("failed", (string)Body(delivery)["status"]); + + // No mapper: what failed was the delivery. + Assert.AreEqual(502, GatewayFailureAnswers.For(GatewayFailureAnswer.Accurate, "x3", hasMapper: false, mapped: false, null).Status); + + var mapping = GatewayFailureAnswers.For(GatewayFailureAnswer.Accurate, "x4", hasMapper: true, mapped: false, null); + Assert.AreEqual(422, mapping.Status); + StringAssert.Contains((string)Body(mapping)["error"], "couldn't be mapped"); + } +} diff --git a/SW.Bitween.UnitTests/GatewayIssuersTests.cs b/SW.Bitween.UnitTests/GatewayIssuersTests.cs index df61924c..02ba82e0 100644 --- a/SW.Bitween.UnitTests/GatewayIssuersTests.cs +++ b/SW.Bitween.UnitTests/GatewayIssuersTests.cs @@ -13,7 +13,7 @@ public class GatewayIssuersTests [TestMethod] public void A_key_the_login_server_stops_publishing_stops_being_accepted() { - // Left to the library's default, it would be accepted for another hour — and a key + // Left to the library's default, it would be accepted for another hour - and a key // dropped that suddenly is usually one that leaked. Assert.IsFalse(new OpenIdGatewayIssuers().For("https://login.example.com").UseLastKnownGoodConfiguration); } diff --git a/SW.Bitween.UnitTests/HttpAdapterIsolationTests.cs b/SW.Bitween.UnitTests/HttpAdapterIsolationTests.cs new file mode 100644 index 00000000..1e87fd4a --- /dev/null +++ b/SW.Bitween.UnitTests/HttpAdapterIsolationTests.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +/// +/// The HTTP adapters share one client per origin across every subscription that calls it. These +/// pin that one subscription's credentials never ride on another's request, and that a message +/// cannot steer a request to another host. +/// +[TestClass] +public class HttpAdapterIsolationTests +{ + [TestMethod] + public async Task A_subscription_without_auth_does_not_send_the_previous_subscriptions_token() + { + var capture = new CapturingHandler(); + var proxy = new SharedClientProxy(new HttpClient(capture)); + + await Handler(proxy, new() { ["Url"] = "https://partner.test/orders", ["AuthType"] = "Bearer", ["LoginPassword"] = "token-of-a" }) + .Handle(new XchangeFile("{}")); + await Handler(proxy, new() { ["Url"] = "https://partner.test/orders" }) + .Handle(new XchangeFile("{}")); + + Assert.AreEqual("Bearer token-of-a", capture.Requests[0].Headers.Authorization?.ToString()); + Assert.IsNull(capture.Requests[1].Headers.Authorization); + } + + [TestMethod] + public async Task An_api_key_is_sent_once_and_not_carried_to_the_next_call() + { + var capture = new CapturingHandler(); + var proxy = new SharedClientProxy(new HttpClient(capture)); + var settings = new Dictionary + { ["Url"] = "https://partner.test/orders", ["AuthType"] = "ApiKey", ["ApiKey"] = "key-of-a" }; + + await Handler(proxy, settings).Handle(new XchangeFile("{}")); + await Handler(proxy, settings).Handle(new XchangeFile("{}")); + await Handler(proxy, new() { ["Url"] = "https://partner.test/orders" }).Handle(new XchangeFile("{}")); + + CollectionAssert.AreEqual(new[] { "key-of-a" }, new List(capture.Requests[1].Headers.GetValues("ApiKey"))); + Assert.IsFalse(capture.Requests[2].Headers.Contains("ApiKey")); + } + + [TestMethod] + public void A_rendered_url_on_the_template_host_is_allowed() => + NativeHttpHandler.EnsureSameOrigin("https://partner.test/orders/{{id}}", new Uri("https://partner.test/orders/42")); + + [TestMethod] + public void A_message_cannot_move_the_request_to_another_host() + { + Assert.ThrowsException(() => + NativeHttpHandler.EnsureSameOrigin("https://partner.test{{path}}", new Uri("https://partner.test.attacker.example/x"))); + // A tenant placeholder fills one label; a value carrying its own host does not fit it. + Assert.ThrowsException(() => + NativeHttpHandler.EnsureSameOrigin("https://{{tenant}}.partner.test/orders", new Uri("https://attacker.example/x"))); + Assert.ThrowsException(() => + NativeHttpHandler.EnsureSameOrigin("https://{{tenant}}.partner.test/orders", new Uri("https://a.b.partner.test/orders"))); + // Nor may a value add credentials or a port the template did not have. + Assert.ThrowsException(() => + NativeHttpHandler.EnsureSameOrigin("https://partner.test{{path}}", new Uri("https://partner.test:8443/x"))); + } + + [TestMethod] + public void A_tenant_placeholder_still_reaches_any_tenant() + { + NativeHttpHandler.EnsureSameOrigin("https://{{tenant}}.partner.test/orders", new Uri("https://acme.partner.test/orders")); + NativeHttpHandler.EnsureSameOrigin("https://partner.test{{path}}", new Uri("https://partner.test/orders/42")); + NativeHttpHandler.EnsureSameOrigin("https://partner.test/{{id}}", new Uri("https://partner.test/42")); + } + + [DataTestMethod] + [DataRow("169.254.169.254", false, true)] + [DataRow("fd00:ec2::254", false, true)] + [DataRow("fe80::1", false, true)] + [DataRow("10.1.2.3", false, false)] + [DataRow("10.1.2.3", true, true)] + [DataRow("192.168.1.10", true, true)] + [DataRow("172.20.0.5", true, true)] + [DataRow("127.0.0.1", true, true)] + [DataRow("::ffff:169.254.169.254", false, true)] + [DataRow("8.8.8.8", true, false)] + [DataRow("2606:4700::1111", true, false)] + public void Addresses_are_refused_as_configured(string address, bool blockPrivate, bool blocked) => + Assert.AreEqual(blocked, OutboundAddressGuard.IsBlocked(IPAddress.Parse(address), blockPrivate)); + + private static NativeHttpHandler Handler(IDynamicHttpProxy proxy, Dictionary settings) + { + var handler = new NativeHttpHandler(proxy); + handler.InitializeStartupValues(settings); + return handler; + } + + private class SharedClientProxy(HttpClient client) : IDynamicHttpProxy + { + public HttpClient GetClient(string fullUrl) => client; + } + + private class CapturingHandler : HttpMessageHandler + { + public List Requests { get; } = new(); + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + // Copy what the client actually sent, default headers included, as the wire would see it. + var sent = new HttpRequestMessage(request.Method, request.RequestUri); + foreach (var header in request.Headers) + sent.Headers.TryAddWithoutValidation(header.Key, header.Value); + Requests.Add(sent); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }); + } + } +} diff --git a/SW.Bitween.UnitTests/HttpHandlerBehaviourTests.cs b/SW.Bitween.UnitTests/HttpHandlerBehaviourTests.cs new file mode 100644 index 00000000..da3ed22a --- /dev/null +++ b/SW.Bitween.UnitTests/HttpHandlerBehaviourTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class HttpHandlerBehaviourTests +{ + [TestInitialize] + public void ForgetTokens() => OAuthTokens.Clear(); + + [DataTestMethod] + [DataRow(HttpStatusCode.TooManyRequests)] + [DataRow(HttpStatusCode.RequestTimeout)] + public async Task Not_now_answers_fail_the_delivery_so_it_can_be_retried(HttpStatusCode status) + { + var server = new Scripted(_ => + { + var response = new HttpResponseMessage(status) { Content = new StringContent("slow down") }; + response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(30)); + return response; + }); + + var ex = await Assert.ThrowsExceptionAsync(() => Handler(server).Handle(new XchangeFile("{}"))); + StringAssert.Contains(ex.Message, "Retry-After: 30s"); + } + + [TestMethod] + public async Task A_client_error_is_still_a_bad_response_not_a_failure() + { + var server = new Scripted(_ => new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent("no") }); + var result = await Handler(server).Handle(new XchangeFile("{}")); + Assert.IsTrue(result.BadData); + } + + [TestMethod] + public async Task Patch_is_sent_as_patch() + { + var server = new Scripted(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }); + await Handler(server, new() { ["Verb"] = "patch" }).Handle(new XchangeFile("{}")); + Assert.AreEqual(HttpMethod.Patch, server.Requests[0].Method); + } + + [TestMethod] + public async Task A_header_value_may_contain_a_colon() + { + var server = new Scripted(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }); + await Handler(server, new() { ["Headers"] = "X-Callback:https://me.example/hook" }).Handle(new XchangeFile("{}")); + CollectionAssert.AreEqual(new[] { "https://me.example/hook" }, + new List(server.Requests[0].Headers.GetValues("X-Callback"))); + } + + [TestMethod] + public async Task An_oauth_token_is_fetched_once_and_reused() + { + var tokenCalls = 0; + var server = new Scripted(request => + { + if (request.RequestUri!.AbsolutePath == "/token") + { + tokenCalls++; + return new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent("{\"access_token\":\"tok\",\"expires_in\":3600}") }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }; + }); + var settings = new Dictionary + { + ["AuthType"] = "OAuth2", ["LoginUrl"] = "https://partner.test/token", ["ClientId"] = "c", ["ClientSecret"] = "s", + }; + + await Handler(server, settings).Handle(new XchangeFile("{}")); + await Handler(server, settings).Handle(new XchangeFile("{}")); + + Assert.AreEqual(1, tokenCalls); + Assert.AreEqual("Bearer tok", server.Requests[^1].Headers.Authorization?.ToString()); + } + + [TestMethod] + public async Task A_failed_token_fetch_is_an_error_not_bearer_null() + { + var server = new Scripted(_ => new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("{}") }); + var settings = new Dictionary + { + ["AuthType"] = "OAuth2", ["LoginUrl"] = "https://partner.test/token", ["ClientId"] = "c", ["ClientSecret"] = "bad", + }; + + var ex = await Assert.ThrowsExceptionAsync(() => Handler(server, settings).Handle(new XchangeFile("{}"))); + StringAssert.Contains(ex.Message, "token endpoint answered 401"); + Assert.AreEqual(1, server.Requests.Count, "nothing was sent with a missing token"); + } + + private static NativeHttpHandler Handler(Scripted server, Dictionary? extra = null) + { + var settings = new Dictionary { ["Url"] = "https://partner.test/orders" }; + foreach (var kv in extra ?? new()) settings[kv.Key] = kv.Value; + var handler = new NativeHttpHandler(new Proxy(new HttpClient(server))); + handler.InitializeStartupValues(settings); + return handler; + } + + private sealed class Proxy(HttpClient client) : IDynamicHttpProxy + { + public HttpClient GetClient(string fullUrl) => client; + } + + private sealed class Scripted(Func answer) : HttpMessageHandler + { + public List Requests { get; } = new(); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Requests.Add(request); + return Task.FromResult(answer(request)); + } + } +} diff --git a/SW.Bitween.UnitTests/MapperContextOptOutTests.cs b/SW.Bitween.UnitTests/MapperContextOptOutTests.cs index e50cb8de..07ad881c 100644 --- a/SW.Bitween.UnitTests/MapperContextOptOutTests.cs +++ b/SW.Bitween.UnitTests/MapperContextOptOutTests.cs @@ -67,7 +67,7 @@ public void MatchIsCaseInsensitive() } /// - /// A serverless mapper is an id no native mapper answers to. It must keep the enrichment — the + /// A serverless mapper is an id no native mapper answers to. It must keep the enrichment - the /// external adapter reads __partner__ out of the payload like any other template. /// [TestMethod] diff --git a/SW.Bitween.UnitTests/MetricsTests.cs b/SW.Bitween.UnitTests/MetricsTests.cs new file mode 100644 index 00000000..fe7617b3 --- /dev/null +++ b/SW.Bitween.UnitTests/MetricsTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Domain.Metrics; +using SW.Bitween.Services.Adapters; +using SW.Bitween.Services.Metrics; + +namespace SW.Bitween.UnitTests; + +/// +/// The in-memory half of the metrics: what is counted, how durations become percentiles, how errors +/// are grouped, and that nothing counted is lost when a flush takes the counts mid-stream. +/// +[TestClass] +public class MetricsTests +{ + [TestMethod] + public void A_percentile_is_the_upper_edge_of_its_bucket_and_never_more_than_the_longest_seen() + { + var stats = new ExchangeStats(); + for (var i = 0; i < 90; i++) stats.AddDuration(40); // < 50 ms + for (var i = 0; i < 9; i++) stats.AddDuration(800); // < 1 s + stats.AddDuration(45_000); // past every edge + + Assert.AreEqual(50, DurationBuckets.Percentile(stats.Buckets(), 0.5, stats.DurationMaxMs)); + Assert.AreEqual(1_000, DurationBuckets.Percentile(stats.Buckets(), 0.95, stats.DurationMaxMs)); + Assert.AreEqual(45_000, DurationBuckets.Percentile(stats.Buckets(), 1.0, stats.DurationMaxMs)); + Assert.IsNull(DurationBuckets.Percentile(new long[10], 0.95, 0)); + + // A bucket's edge says "under 50 ms"; when everything took 3 ms, it says 3. + var quick = new ExchangeStats(); + quick.AddDuration(3); + Assert.AreEqual(3, DurationBuckets.Percentile(quick.Buckets(), 0.95, quick.DurationMaxMs)); + } + + [TestMethod] + public void Errors_that_differ_only_in_their_details_are_one_kind() + { + var a = ErrorFingerprint.Of("Order 1043 not found at https://acme.test/orders/1043 (id 7c9e6679-7425-40de-944b-e07fc1f90ae7)"); + var b = ErrorFingerprint.Of("Order 2210 not found at https://acme.test/orders/2210 (id 0f8fad5b-d9cb-469f-a165-70867728950e)"); + var c = ErrorFingerprint.Of("Connection refused"); + Assert.AreEqual(a, b); + Assert.AreNotEqual(a, c); + Assert.AreEqual("first line", ErrorFingerprint.Sample("\n first line \n at Some.Stack()")); + } + + [TestMethod] + public void Exchanges_are_counted_by_hour_subscription_and_partner() + { + var recorder = new MetricsRecorder(); + var started = new DateTime(2026, 10, 11, 9, 15, 0, DateTimeKind.Utc); + recorder.Exchange(started, 5, 7, ExchangeOutcome.Succeeded, FailedAt.Elsewhere, false, false, 120, 1000, 400, null); + recorder.Exchange(started.AddMinutes(30), 5, 7, ExchangeOutcome.Failed, FailedAt.Handler, true, false, 900, 1000, 0, "Timed out talking to Acme"); + recorder.Exchange(started.AddHours(1), 5, 7, ExchangeOutcome.BadResponse, FailedAt.Elsewhere, false, true, 300, 10, 20, null); + + var snapshot = recorder.Drain(); + var nine = snapshot.Exchanges.Single(s => s.Hour.Hour == 9); + Assert.AreEqual((2, 1, 1, 1, 1), (nine.Total, nine.Succeeded, nine.Failed, nine.FailedAtHandler, nine.Retries)); + Assert.AreEqual((2000L, 400L), (nine.BytesIn, nine.BytesOut)); + var ten = snapshot.Exchanges.Single(s => s.Hour.Hour == 10); + Assert.AreEqual((1, 1, 1), (ten.Total, ten.BadResponse, ten.ManualRetries)); + Assert.AreEqual("5", snapshot.Errors.Single().ScopeKey); + Assert.IsTrue(recorder.Drain().IsEmpty, "drained counts are gone from the recorder"); + } + + [TestMethod] + public async Task Nothing_counted_while_a_flush_drains_is_lost() + { + var recorder = new MetricsRecorder(); + var started = DateTime.UtcNow; + var counted = new List(); + var writers = Enumerable.Range(0, 8).Select(_ => Task.Run(() => + { + for (var i = 0; i < 5_000; i++) + recorder.Exchange(started, 1, 0, ExchangeOutcome.Succeeded, FailedAt.Elsewhere, false, false, 10, 1, 1, null); + })).ToArray(); + while (!writers.All(w => w.IsCompleted)) + { + counted.Add(recorder.Drain()); + await Task.Delay(1); + } + counted.Add(recorder.Drain()); + Assert.AreEqual(40_000, counted.SelectMany(s => s.Exchanges).Sum(s => s.Total)); + } + + [TestMethod] + public void Counts_a_flush_could_not_write_go_with_the_next_one() + { + var recorder = new MetricsRecorder(); + recorder.Exchange(DateTime.UtcNow, 3, 0, ExchangeOutcome.Succeeded, FailedAt.Elsewhere, false, false, 10, 1, 1, null); + var failed = recorder.Drain(); + recorder.Exchange(DateTime.UtcNow, 3, 0, ExchangeOutcome.Succeeded, FailedAt.Elsewhere, false, false, 10, 1, 1, null); + recorder.Restore(failed); + Assert.AreEqual(2, recorder.Drain().Exchanges.Single().Total); + } + + [TestMethod] + public void A_failed_adapter_call_is_told_apart_by_how_it_failed() + { + Assert.AreEqual(AdapterFailure.None, AdapterInvoker.Classify(null)); + Assert.AreEqual(AdapterFailure.Timeout, AdapterInvoker.Classify(new TimeoutException())); + Assert.AreEqual(AdapterFailure.Crash, AdapterInvoker.Classify(new IOException("Adapter stream closed."))); + Assert.AreEqual(AdapterFailure.Crash, AdapterInvoker.Classify(new Exception("Process not started or terminated."))); + Assert.AreEqual(AdapterFailure.Error, AdapterInvoker.Classify(new InvalidOperationException("Partner said no"))); + } + + [TestMethod] + public async Task Every_adapter_call_is_timed_and_counted_with_its_runtime_role_and_version() + { + var recorder = new MetricsRecorder(); + var invoker = new AdapterInvoker([new ThrowingRuntime()], recorder); + + await invoker.InvokeAsync("Acme.Handlers.Orders/1.2.0", AdapterRole.Handler, "Handle", null); + await Assert.ThrowsExceptionAsync(() => + invoker.InvokeAsync("acme.handlers.orders/1.2.0", AdapterRole.Handler, "Slow", null)); + + var row = recorder.Drain().Adapters.Single(); + Assert.AreEqual(("acme.handlers.orders", "1.2.0", "handler"), (row.AdapterId, row.Version, row.Role)); + Assert.AreEqual((2, 1, 1), (row.Calls, row.Failures, row.Timeouts)); + } + + sealed class ThrowingRuntime : IAdapterRuntime + { + public Task CanRunAsync(string adapterId) => Task.FromResult(true); + + public Task BeginAsync(string adapterId, AdapterRole role, IDictionary properties, + string correlationId) => Task.FromResult(new Session()); + + sealed class Session : IAdapterSession + { + public Task InvokeAsync(string method, object argument = null) => + method == "Slow" ? throw new TimeoutException("too slow") : Task.FromResult(default(TResult)); + + public Task InvokeAsync(string method, object argument = null) => Task.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } + } +} diff --git a/SW.Bitween.UnitTests/MicrosoftTokenValidationTests.cs b/SW.Bitween.UnitTests/MicrosoftTokenValidationTests.cs new file mode 100644 index 00000000..1301d525 --- /dev/null +++ b/SW.Bitween.UnitTests/MicrosoftTokenValidationTests.cs @@ -0,0 +1,75 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +/// +/// A Microsoft ID token signs someone in only when it was issued to this Bitween's app, by the +/// configured tenant. Neither was checked before: a token issued to any app, anywhere, would do. +/// +[TestClass] +public class MicrosoftTokenValidationTests +{ + private const string ClientId = "11111111-1111-1111-1111-111111111111"; + private const string Tenant = "22222222-2222-2222-2222-222222222222"; + private const string OtherTenant = "33333333-3333-3333-3333-333333333333"; + + private static readonly RsaSecurityKey Key = new(RSA.Create(2048)) { KeyId = "test" }; + + private static string Token(string audience = ClientId, string tenant = Tenant, string issuerTenant = null, + DateTime? expires = null, SecurityKey signWith = null) => + new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken( + issuer: $"https://login.microsoftonline.com/{issuerTenant ?? tenant}/v2.0", + audience: audience, + claims: + [ + new Claim("preferred_username", "Person@Example.com"), + new Claim("oid", "aaaa-bbbb"), + new Claim("tid", tenant), + ], + notBefore: DateTime.UtcNow.AddMinutes(-20), + expires: expires ?? DateTime.UtcNow.AddMinutes(30), + signingCredentials: new SigningCredentials(signWith ?? Key, SecurityAlgorithms.RsaSha256))); + + private static AccountExtensions.MicrosoftIdentity Validate(string token, string tenantId = Tenant) => + AccountExtensions.ValidateMicrosoftToken(token, [Key], ClientId, tenantId); + + [TestMethod] + public void A_token_for_this_app_from_its_tenant_signs_in() + { + var identity = Validate(Token()); + Assert.AreEqual("person@example.com", identity.Email); + Assert.AreEqual($"aaaa-bbbb@{Tenant}", identity.ObjectAndTenant); + } + + [TestMethod] + public void A_token_issued_to_another_app_is_refused() => + Assert.ThrowsException(() => Validate(Token(audience: "another-app"))); + + [TestMethod] + public void A_token_from_another_tenant_is_refused_when_the_tenant_is_pinned() => + Assert.ThrowsException(() => Validate(Token(tenant: OtherTenant))); + + [TestMethod] + public void A_multi_tenant_setup_accepts_any_tenant() => + Assert.IsNotNull(Validate(Token(tenant: OtherTenant), tenantId: "common")); + + [TestMethod] + public void An_issuer_that_does_not_match_the_token_tenant_is_refused() => + Assert.ThrowsException(() => + Validate(Token(tenant: Tenant, issuerTenant: OtherTenant), tenantId: "common")); + + [TestMethod] + public void An_expired_token_is_refused() => + Assert.ThrowsException(() => + Validate(Token(expires: DateTime.UtcNow.AddMinutes(-10)))); + + [TestMethod] + public void A_token_signed_by_someone_else_is_refused() => + Assert.ThrowsException(() => + Validate(Token(signWith: new RsaSecurityKey(RSA.Create(2048)) { KeyId = "intruder" }))); +} diff --git a/SW.Bitween.UnitTests/MigrationDriftTests.cs b/SW.Bitween.UnitTests/MigrationDriftTests.cs index 1a7c8d07..b0eec052 100644 --- a/SW.Bitween.UnitTests/MigrationDriftTests.cs +++ b/SW.Bitween.UnitTests/MigrationDriftTests.cs @@ -15,7 +15,7 @@ namespace SW.Bitween.UnitTests; /// /// Every provider is covered deliberately. MsSql and MySql inherit the base context's seed data /// (they call base.OnModelCreating; PgSql re-declares the model instead), so a change to shared -/// seed values silently drifts their snapshots even when PgSql's has been regenerated — which is +/// seed values silently drifts their snapshots even when PgSql's has been regenerated - which is /// exactly what happened when the seed timestamp fix was first applied to PgSql alone. /// /// No database is touched: this compares the in-memory model against the compiled-in snapshot, @@ -42,7 +42,7 @@ private static void AssertNoPendingChanges(DbContext dbContext) { Assert.IsFalse(dbContext.Database.HasPendingModelChanges(), $"{dbContext.GetType().FullName} has model changes that no migration covers. " - + "The app will refuse to start against this provider. Generate a migration for it — " + + "The app will refuse to start against this provider. Generate a migration for it - " + "and remember a shared-model change usually needs one per provider."); } } @@ -77,7 +77,7 @@ private static MsSql.BitweenDbContext BuildMsSql() private static MySql.BitweenDbContext BuildMySql() { - // An explicit server version, never AutoDetect — the latter opens a connection. + // An explicit server version, never AutoDetect - the latter opens a connection. var options = new DbContextOptionsBuilder() .UseMySql("Server=unused;Database=unused;User=unused;Password=unused", new MySqlServerVersion(new Version(8, 0, 18)), diff --git a/SW.Bitween.UnitTests/NativeAdapterManifestTests.cs b/SW.Bitween.UnitTests/NativeAdapterManifestTests.cs new file mode 100644 index 00000000..1a1f31dc --- /dev/null +++ b/SW.Bitween.UnitTests/NativeAdapterManifestTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters; +using SW.Bitween.Services.Adapters; +using SW.Serverless.Contract.Catalog; + +namespace SW.Bitween.UnitTests; + +/// +/// Every native adapter carries a manifest, and the manifest says what its code says. The settings a +/// native adapter is configured with come from its startup-values type at run time; a manifest that +/// listed different ones would show a marketplace something the adapter does not do. +/// +[TestClass] +public class NativeAdapterManifestTests +{ + static readonly Type[] NativeTypes = typeof(INativeAdapter).Assembly.GetTypes() + .Where(t => !t.IsAbstract && typeof(INativeAdapter).IsAssignableFrom(t)) + .ToArray(); + + static readonly Dictionary Manifests = + NativeAdapterManifests.Read(typeof(INativeAdapter).Assembly) + .ToDictionary(m => m.Id, m => m.Manifest, StringComparer.OrdinalIgnoreCase); + + [TestMethod] + public void Every_native_adapter_has_a_valid_manifest_named_after_it() + { + foreach (var type in NativeTypes) + { + Assert.IsTrue(Manifests.TryGetValue(type.Name, out var manifest), $"{type.Name} has no adapter.json"); + Assert.AreEqual(type.Name.ToLowerInvariant(), manifest.Id); + Assert.AreEqual(0, manifest.Validate().Count, $"{type.Name}: {string.Join("; ", manifest.Validate())}"); + Assert.IsFalse(string.IsNullOrWhiteSpace(manifest.DisplayName), $"{type.Name} has no display name"); + Assert.IsFalse(string.IsNullOrWhiteSpace(manifest.Summary), $"{type.Name} has no summary"); + Assert.AreEqual(1, manifest.Kinds.Count, $"{type.Name} should declare its one kind"); + } + } + + [TestMethod] + public void A_manifest_lists_exactly_the_settings_its_adapter_reads() + { + foreach (var type in NativeTypes) + { + var manifest = Manifests[type.Name]; + var startupType = (Type)type.GetProperty(nameof(INativeAdapter.StartupValuesType))! + .GetValue(RuntimeHelpers.GetUninitializedObject(type)); + var defaults = Activator.CreateInstance(startupType); + + var fromCode = startupType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name, p => + { + var hasDefault = p.GetValue(defaults) != null; + var nullable = !p.PropertyType.IsValueType || Nullable.GetUnderlyingType(p.PropertyType) != null; + var required = p.GetCustomAttribute() != null || (!nullable && !hasDefault); + return (Required: required, Secret: p.GetCustomAttribute() != null); + }, StringComparer.OrdinalIgnoreCase); + + CollectionAssert.AreEquivalent( + fromCode.Keys.Select(k => k.ToLowerInvariant()).ToList(), + manifest.Properties.Select(p => p.Name.ToLowerInvariant()).ToList(), + $"{type.Name}: the manifest's properties differ from {startupType.Name}'s"); + + foreach (var property in manifest.Properties) + { + var code = fromCode[property.Name]; + Assert.AreEqual(code.Secret, property.Secret, $"{type.Name}.{property.Name}: secret differs from the code"); + Assert.AreEqual(code.Required, property.Required, $"{type.Name}.{property.Name}: required differs from the code"); + if (property.Secret) + Assert.IsNull(property.Default, $"{type.Name}.{property.Name}: a secret's default must not be published"); + } + } + } +} diff --git a/SW.Bitween.UnitTests/NativeMapper/CsvFormatTests.cs b/SW.Bitween.UnitTests/NativeMapper/CsvFormatTests.cs index 3a599c73..0c3a61cb 100644 --- a/SW.Bitween.UnitTests/NativeMapper/CsvFormatTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/CsvFormatTests.cs @@ -202,7 +202,7 @@ public void Whitespace_inside_a_field_is_kept() [TestMethod] public void A_repeated_column_name_stays_reachable_under_a_name_of_its_own() { - // Two columns cannot share a name — a row is built by setting keys on an object, so the + // Two columns cannot share a name - a row is built by setting keys on an object, so the // second would silently replace the first and a column would simply be gone. Numbered // rather than moved to its position, which reads better and cannot collide with a header // that happens to be a number. @@ -216,7 +216,7 @@ public void A_repeated_column_name_stays_reachable_under_a_name_of_its_own() [TestMethod] public void A_fallback_name_that_a_header_already_took_is_moved_out_of_the_way() { - // The header's second column has no name, so it wants its position — which the first + // The header's second column has no name, so it wants its position - which the first // column is already called. Sharing it would mean the second column silently replacing // the first when the row is built, and a column of the file simply disappearing. var rows = Rows("2,,qty\na,b,7"); @@ -349,7 +349,7 @@ public void An_object_is_a_single_row() public void A_nested_field_becomes_a_dotted_column() { // The editor already shows the path this way, and the output-field name box splits what is - // typed into it on dots — so this is the only way such a column can exist at all. + // typed into it on dots - so this is the only way such a column can exist at all. var written = Written(L(Obj( ("orderId", V("A1")), ("destination", Obj(("city", V("Amman")), ("country", V("JO"))))))); diff --git a/SW.Bitween.UnitTests/NativeMapper/CsvMappingTests.cs b/SW.Bitween.UnitTests/NativeMapper/CsvMappingTests.cs index 8c06066f..c1ca637d 100644 --- a/SW.Bitween.UnitTests/NativeMapper/CsvMappingTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/CsvMappingTests.cs @@ -14,7 +14,7 @@ namespace SW.Bitween.UnitTests.NativeMapper; /// /// These are the tests that decide whether the design claim holds: that delimited text needs /// nothing from the engine, because a file is a list of rows and the mapper already walks -/// lists. Nothing here configures a CSV feature — the rules are the same ones a JSON root array +/// lists. Nothing here configures a CSV feature - the rules are the same ones a JSON root array /// uses, and the only thing that changed is which reader produced the tree. /// /// @@ -72,7 +72,7 @@ private static JToken Run(MappingRules rules, string document, CsvOptions source [TestMethod] public void A_file_with_a_header_maps_by_column_name() { - // The whole output is the list, walking the document itself — the same two settings a JSON + // The whole output is the list, walking the document itself - the same two settings a JSON // bare array already uses, written before delimited text existed. var rules = new MappingRules { @@ -166,7 +166,7 @@ public void A_field_can_be_made_a_real_number_where_the_output_wants_one() [TestMethod] public void A_delimited_file_can_be_mapped_into_another_one() { - // The pipe file becoming the comma file, which is a mapping like any other — one format's + // The pipe file becoming the comma file, which is a mapping like any other - one format's // reader and another's writer, even when they are the same format configured differently. var rules = new MappingRules { @@ -260,7 +260,7 @@ public void The_rules_never_mention_a_format_so_the_writer_alone_decides() [TestMethod] public void The_whole_file_can_be_produced_header_rows_and_trailer() { - // The end of it: reading the carrier's file was never the hard half. This writes one — + // The end of it: reading the carrier's file was never the hard half. This writes one - // an H line, a D line per shipment, and a T line carrying how many there were. var rules = new MappingRules { @@ -281,7 +281,7 @@ public void The_whole_file_can_be_produced_header_rows_and_trailer() }, ], Fields = [Field("1", "1"), Field("2", "2"), Field("3", "3")], - // The trailer, written after them — and the only place the count is known. + // The trailer, written after them - and the only place the count is known. After = [ new ListEntry @@ -317,7 +317,7 @@ public void The_count_does_not_move_when_a_header_line_is_added() { // The reason it counts rows rather than entries. Building the trailer first and adding // the header afterwards is the ordinary way round, and the number must not shift under - // it — nothing on screen would say that it had. + // it - nothing on screen would say that it had. var trailer = new ListEntry { Fields = [new FieldRule { Target = ["count"], From = Counted() }], diff --git a/SW.Bitween.UnitTests/NativeMapper/DocumentMapperTests.cs b/SW.Bitween.UnitTests/NativeMapper/DocumentMapperTests.cs index 33ecf609..59b7f990 100644 --- a/SW.Bitween.UnitTests/NativeMapper/DocumentMapperTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/DocumentMapperTests.cs @@ -78,7 +78,7 @@ public void FixedSource_UsesTheLiteral() } /// - /// Partner values come from the context, never from the payload — that is the whole point of the + /// Partner values come from the context, never from the payload - that is the whole point of the /// new mapper being handed its context instead of having it written into the document. /// [TestMethod] @@ -97,7 +97,7 @@ public void PartnerSource_ReadsTheContext() } /// - /// Source values come from the context, by the path the rule reads — read out of the delivered + /// Source values come from the context, by the path the rule reads - read out of the delivered /// document when the exchange was created, not out of the document being mapped. /// [TestMethod] @@ -239,7 +239,7 @@ public void NestedTarget_BuildsTheObjects() } /// - /// Output order is the order of the rules, not of the source document and not of any sample — + /// Output order is the order of the rules, not of the source document and not of any sample - /// which is what makes it deterministic and what XML sequences require. /// [TestMethod] @@ -339,7 +339,7 @@ public void Lookup_IsAskedAboutTheTransformedValue() } /// - /// How an ambiguous date is read is a setting on the mapping, not on the rule — a partner writes + /// How an ambiguous date is read is a setting on the mapping, not on the rule - a partner writes /// dates one way throughout. The transforms have their own tests; this pins that the mapping's /// setting is what reaches them. /// @@ -789,7 +789,7 @@ public void FixedEntries_ComeBeforeTheWalkedOnes() } /// - /// A fixed entry has no entry of its own, so its paths read the scope the list sits in — + /// A fixed entry has no entry of its own, so its paths read the scope the list sits in - /// which is what lets one carry a value from the document. /// [TestMethod] @@ -845,7 +845,7 @@ public void AFixedEntry_ReadsThePartner() /// /// No source list to walk: the list is exactly its fixed entries. This is what the previous - /// mapper called a primitive array — one slot per rule rather than one per source entry. + /// mapper called a primitive array - one slot per rule rather than one per source entry. /// [TestMethod] public void NoSourceList_GivesExactlyTheFixedEntries() @@ -1021,7 +1021,7 @@ public void ACoercionError_DoesNotQuoteAPartnerOrGlobalValue() var ex = Assert.ThrowsException(() => Map(rules, Order, context)); // The message is stored on the exchange and returned by the preview API, and an adapter - // password is configuration rather than payload — so its length is said, not its value. + // password is configuration rather than payload - so its length is said, not its value. Assert.IsFalse(ex.Message.Contains("s3cr3t-value"), ex.Message); Assert.IsFalse(ex.Message.Contains("also-secret"), ex.Message); StringAssert.Contains(ex.Message, "12 characters"); diff --git a/SW.Bitween.UnitTests/NativeMapper/ParityWithJsonMapperTests.cs b/SW.Bitween.UnitTests/NativeMapper/ParityWithJsonMapperTests.cs index 5e77534f..41b0b196 100644 --- a/SW.Bitween.UnitTests/NativeMapper/ParityWithJsonMapperTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/ParityWithJsonMapperTests.cs @@ -23,20 +23,20 @@ namespace SW.Bitween.UnitTests.NativeMapper; /// /// /// -/// TrailingCommas_AreRemovedFromRenderedJson and InvalidScribanTemplate — there is no +/// TrailingCommas_AreRemovedFromRenderedJson and InvalidScribanTemplate - there is no /// template, so neither situation exists. /// /// -/// SmartArray_AllowsFirstItemMemberAccessWithoutIndex — a list no longer pretends to be its +/// SmartArray_AllowsFirstItemMemberAccessWithoutIndex - a list no longer pretends to be its /// own first element. That was ambiguous: order.line.sku silently meant the first line's sku, /// with no way to ask for anything else. A path into a list belongs to a loop. /// /// -/// SourceMapping_ResolvesLowercaseAliasForPascalCaseKey — every key no longer gets a +/// SourceMapping_ResolvesLowercaseAliasForPascalCaseKey - every key no longer gets a /// camelCase twin. A path matches the document exactly. /// /// -/// The TypeScript round-trip tests — they test parsing a template back into rules, which is the +/// The TypeScript round-trip tests - they test parsing a template back into rules, which is the /// thing this design removes. /// /// @@ -200,7 +200,7 @@ public void MissingPartnerOrGlobalKeyIsNull() } /// - /// Old: PartnerAndGlobalTypedTargets_AreRepresentedAsNullTemplateLiteral — the old + /// Old: PartnerAndGlobalTypedTargets_AreRepresentedAsNullTemplateLiteral - the old /// generator gave up and emitted a literal null for a partner value on a number target, because /// it could not know at generation time whether the text would parse. Here the value exists when /// the conversion runs, so a numeric partner value converts and a non-numeric one is reported. @@ -313,7 +313,7 @@ public void TypeConversionsMatchTheOldRules() /// /// Old: TypeRules_StringToBool_IsNull and StringToNumber_InvalidIsNull. The old - /// mapper produced null; this one fails and names the rule. Deliberate — a partner receiving a + /// mapper produced null; this one fails and names the rule. Deliberate - a partner receiving a /// document with a silent hole in it is worse than being told the mapping is wrong. /// [TestMethod] @@ -334,7 +334,7 @@ public void ImpossibleConversionsFailInsteadOfProducingNull() // ── transforms ────────────────────────────────────────────────────────────── - /// Old: TransformMapping_AppliesMathExpression — a Scriban expression then. + /// Old: TransformMapping_AppliesMathExpression - a Scriban expression then. [TestMethod] public void AppliesArithmetic() => Assert.AreEqual(116m, @@ -585,7 +585,7 @@ public void RootArrayInputToRootArrayOutput() Assert.AreEqual("B7", output[1]["code"]?.ToString()); } - /// Old: RootArrayInput_AccessFirstItemField_ViaItemsIndex — reading a root array + /// Old: RootArrayInput_AccessFirstItemField_ViaItemsIndex - reading a root array /// into an object output, which the old mapper did by hoisting the first element's fields. [TestMethod] public void RootArrayInputToObjectOutput() diff --git a/SW.Bitween.UnitTests/NativeMapper/RulesSerializationTests.cs b/SW.Bitween.UnitTests/NativeMapper/RulesSerializationTests.cs index a5c9f142..f0d8e56f 100644 --- a/SW.Bitween.UnitTests/NativeMapper/RulesSerializationTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/RulesSerializationTests.cs @@ -11,7 +11,7 @@ namespace SW.Bitween.UnitTests.NativeMapper; /// /// /// Pinned because both sides depend on it and neither can see the other's types. The editor writes -/// what a JavaScript developer would write — camelCase names, lowercase enum values — so these +/// what a JavaScript developer would write - camelCase names, lowercase enum values - so these /// tests exist to make sure that keeps working rather than being an accident of Newtonsoft's /// defaults. /// @@ -134,7 +134,7 @@ public void NestedLoopsAndItemRules() public void RootLoop_ForRootArrayOutput() => Assert.AreEqual("lines", Read("""{ "root": { "over": "lines" } }""").Root!.Over); - /// Empty rules are valid — a mapping being built has none yet. + /// Empty rules are valid - a mapping being built has none yet. [TestMethod] public void EmptyObject_GivesDefaults() { @@ -174,7 +174,7 @@ public void RoundTripsThroughSerialisation() /// /// Fixed entries, and the three states of over. Absent and empty mean different things - /// — nothing walked, versus the document is the list — so a serialiser that flattened one into + /// - nothing walked, versus the document is the list - so a serialiser that flattened one into /// the other would quietly change what a mapping produces. /// [TestMethod] diff --git a/SW.Bitween.UnitTests/NativeMapper/TransformsTests.cs b/SW.Bitween.UnitTests/NativeMapper/TransformsTests.cs index 55d1d9d2..63092b42 100644 --- a/SW.Bitween.UnitTests/NativeMapper/TransformsTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/TransformsTests.cs @@ -41,7 +41,7 @@ private static string AssertFails(TransformRule rule, object? value, /// /// Every name the editor can offer must actually resolve, and every implemented function must be - /// listed — a name in one and not the other is a dropdown entry that fails at runtime, or a + /// listed - a name in one and not the other is a dropdown entry that fails at runtime, or a /// working function nobody can find. /// [TestMethod] @@ -131,7 +131,7 @@ public void Concat() => [TestMethod] public void Multiply() => Assert.AreEqual(116m, Apply(Rule("multiply", ("by", 1.16)), 100m)); - /// A number arriving as text — every value from XML or CSV does — still multiplies. + /// A number arriving as text - every value from XML or CSV does - still multiplies. [TestMethod] public void Multiply_OnANumericString() => Assert.AreEqual(116m, Apply(Rule("multiply", ("by", 1.16)), "100")); @@ -203,7 +203,7 @@ public void FormatDate_UnreadableDate_Fails() => /// /// A real CargoNet shipping date. It is the 4th of September, and the invariant parser - /// reads it as the 9th of April without complaining — so a date that is not year-first + /// reads it as the 9th of April without complaining - so a date that is not year-first /// is refused until the rule says which way round it is. /// [TestMethod] @@ -242,7 +242,7 @@ public void FormatDate_TheSameTextReadsBothWays() } /// - /// Year-first needs no telling — there is nothing to get wrong — so existing mappings and + /// Year-first needs no telling - there is nothing to get wrong - so existing mappings and /// every ISO timestamp keep working untouched. /// [TestMethod] @@ -265,7 +265,7 @@ public void FormatDate_AnOrderStillAcceptsAnIsoValue() => DateOrder.DayFirst)); /// - /// 13 cannot be a month, so this one is not actually ambiguous — but accepting it while + /// 13 cannot be a month, so this one is not actually ambiguous - but accepting it while /// refusing 04.09.2026 would mean the same format worked for some of a partner's rows and /// not others. One rule, whatever the values happen to be. /// @@ -315,7 +315,7 @@ public void AnArgumentThatThrows_IsReportedRatherThanEscaping() { // A substring index far outside int range, saved in a rule nobody validated on the way // in. The cast to int overflows; every other failure in this class comes back through - // `error`, and DocumentMapper only aggregates the ones that do — an exception here would + // `error`, and DocumentMapper only aggregates the ones that do - an exception here would // abort the whole mapping at the first bad rule and surface as a stack trace. var rule = new TransformRule { Fn = "substring" }; rule.Args["start"] = JToken.FromObject(99999999999999m); diff --git a/SW.Bitween.UnitTests/NativeMapper/ValuesTests.cs b/SW.Bitween.UnitTests/NativeMapper/ValuesTests.cs index 4713d477..0183f50c 100644 --- a/SW.Bitween.UnitTests/NativeMapper/ValuesTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/ValuesTests.cs @@ -12,6 +12,30 @@ namespace SW.Bitween.UnitTests.NativeMapper; [TestClass] public class ValuesCoerceTests { + /// + /// Commas count only as thousands separators in groups of three ahead of the decimal point. + /// "1,5" - one and a half, written the European way - used to become fifteen with no error. + /// + [DataTestMethod] + [DataRow("1,5")] + [DataRow("12,34")] + [DataRow("1,2345")] + [DataRow("1.5,3")] + [DataRow(",123")] + public void A_comma_that_is_not_a_thousands_separator_is_not_a_number(string text) => + AssertCannotCoerce(text, ValueType.Number); + + [DataTestMethod] + [DataRow("1,234", "1234")] + [DataRow("1,234,567.89", "1234567.89")] + [DataRow("-12,345", "-12345")] + [DataRow("(100)", "-100")] + [DataRow("1.5", "1.5")] + [DataRow(" 42 ", "42")] + public void Ordinary_numbers_still_convert(string text, string expected) => + Assert.AreEqual(decimal.Parse(expected, System.Globalization.CultureInfo.InvariantCulture), + Coerce(text, ValueType.Number)); + private static object? Coerce(object? value, ValueType? type) { Assert.IsTrue(Values.TryCoerce(value, type, out var result), $"expected '{value}' to convert to {type}"); @@ -28,7 +52,7 @@ private static void AssertCannotCoerce(object? value, ValueType type) // ── null ──────────────────────────────────────────────────────────────────── /// - /// A missing source field is an absent value, not a failure — optional fields are ordinary, and + /// A missing source field is an absent value, not a failure - optional fields are ordinary, and /// the writer decides how to represent absence. /// [TestMethod] diff --git a/SW.Bitween.UnitTests/NativeMapper/XmlFormatTests.cs b/SW.Bitween.UnitTests/NativeMapper/XmlFormatTests.cs index 639e9f57..e4469cee 100644 --- a/SW.Bitween.UnitTests/NativeMapper/XmlFormatTests.cs +++ b/SW.Bitween.UnitTests/NativeMapper/XmlFormatTests.cs @@ -9,8 +9,8 @@ namespace SW.Bitween.UnitTests.NativeMapper; /// Reading XML into the neutral tree. /// /// -/// The shapes here are taken from documents real partners send — SOAP requests and responses from -/// two carriers — with the credentials replaced. Every one of them was in the samples: the same URI +/// The shapes here are taken from documents real partners send - SOAP requests and responses from +/// two carriers - with the credentials replaced. Every one of them was in the samples: the same URI /// bound to a prefix and to the default at once, xmlns="" putting a subtree back into no /// namespace, empty elements by the dozen, and a postcode with a leading space. /// @@ -132,7 +132,7 @@ public void An_empty_element_reads_as_present_and_empty() var tree = Format.Read("x"); // `` and `` are the same document, so they must read the same. Empty rather than - // missing, because the element is there — and a rule can tell the two apart. + // missing, because the element is there - and a rule can tell the two apart. Assert.AreEqual("", At(tree, "v.a")); Assert.AreEqual("", At(tree, "v.b")); Assert.IsNull(NodeAt(tree, "v.missing"), "an element that is not there reads as nothing"); @@ -343,7 +343,7 @@ public void Three_repeated_elements_are_three_entries() public void One_element_is_one_entry_not_none() { // The whole reason the trait exists. `` used once reads as an object, so without it - // the list rule finds no list and produces nothing — losing the only line in silence. + // the list rule finds no list and produces nothing - losing the only line in silence. var lines = Lines(Xml, "A1", "order.line"); CollectionAssert.AreEqual(new[] { "A1" }, Codes(lines)); @@ -361,7 +361,7 @@ public void An_element_that_is_not_there_is_still_no_entries() public void The_document_itself_is_not_a_list_of_one() { // `over: ""` says the whole document is the list, which is a claim about a root array - // and never true of XML — a document has exactly one root, and it is not a repeated + // and never true of XML - a document has exactly one root, and it is not a repeated // element. It is also what a list rule holds before a source list has been chosen, so // forgiving it here made an unconfigured list quietly produce one entry: the document, // with every field inside it resolving to nothing. @@ -385,7 +385,7 @@ public void Json_is_unchanged_because_json_says_which() /// /// The conventions are the reader's, in reverse: @name becomes an attribute, #text /// becomes the element's own text, and a list becomes the same element repeated. Namespaces are -/// ordinary @xmlns fields, which is what they are in XML — so declaring one needs no +/// ordinary @xmlns fields, which is what they are in XML - so declaring one needs no /// machinery of its own, and the prefixes in the output are the ones the mapping asked for. /// [TestClass] @@ -483,7 +483,7 @@ public void A_list_inside_a_list_is_refused() public void An_empty_or_null_value_is_an_empty_element() { // The two are the same document. XML has no null, and an element with no text is all it can - // say — which is also what the partner's own documents are full of. + // say - which is also what the partner's own documents are full of. var xml = Written(Obj(("v", Obj( ("a", ValueNode.Value(null)), ("b", V("")))))); diff --git a/SW.Bitween.UnitTests/NativePop3ReceiverTests.cs b/SW.Bitween.UnitTests/NativePop3ReceiverTests.cs index 192a44dc..c6947d17 100644 --- a/SW.Bitween.UnitTests/NativePop3ReceiverTests.cs +++ b/SW.Bitween.UnitTests/NativePop3ReceiverTests.cs @@ -123,4 +123,60 @@ public async Task DeleteFile_MarksMessageDeletedOnServer() CollectionAssert.Contains(server.DeletedMessageNumbers.ToList(), 1); } + + static System.Security.Cryptography.X509Certificates.X509Certificate2 SelfSigned() + { + using var key = System.Security.Cryptography.RSA.Create(2048); + var request = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=mail.lab.local", key, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30)); + // Exported and read back, so the server can use its private key on every platform. + return System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadPkcs12(cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx), null); + } + + static NativePop3Receiver Pinned(FakePop3Server server, string? thumbprint) + { + var receiver = new NativePop3Receiver(); + var settings = Settings(); + settings["Port"] = server.Port.ToString(); + if (thumbprint != null) settings["CertificateThumbprint"] = thumbprint; + receiver.InitializeStartupValues(settings); + return receiver; + } + + [TestMethod] + public async Task A_pinned_certificate_is_trusted_though_self_signed() + { + using var cert = SelfSigned(); + using var server = new FakePop3Server(new[] { Pop3TestMessages.Plain }, tls: cert); + var thumbprint = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(cert.RawData)); + var receiver = Pinned(server, "SHA256:" + thumbprint.ToLowerInvariant()); + + await receiver.Initialize(); + Assert.AreEqual(1, (await receiver.ListFiles()).Count()); + await receiver.Finalize(); + } + + [TestMethod] + public async Task Another_certificate_than_the_pinned_one_is_refused_before_the_password_goes() + { + using var cert = SelfSigned(); + using var server = new FakePop3Server(new[] { Pop3TestMessages.Plain }, tls: cert); + var receiver = Pinned(server, new string('A', 64)); + + var refused = await Assert.ThrowsExceptionAsync(() => receiver.Initialize()); + StringAssert.Contains(refused.Message, "not the one configured"); + Assert.IsFalse(server.PasswordSent); + } + + [TestMethod] + public async Task Without_a_pin_a_self_signed_certificate_is_refused_as_before() + { + using var cert = SelfSigned(); + using var server = new FakePop3Server(new[] { Pop3TestMessages.Plain }, tls: cert); + var receiver = Pinned(server, null); + + await Assert.ThrowsExceptionAsync(() => receiver.Initialize()); + Assert.IsFalse(server.PasswordSent); + } } diff --git a/SW.Bitween.UnitTests/NativeSmtpHandlerPinningTests.cs b/SW.Bitween.UnitTests/NativeSmtpHandlerPinningTests.cs new file mode 100644 index 00000000..e185f376 --- /dev/null +++ b/SW.Bitween.UnitTests/NativeSmtpHandlerPinningTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters.SmtpHandler; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +/// +/// The SMTP handler and a relay with its own certificate: trusted when that certificate is pinned, +/// refused before anything is sent when another is presented, and refused as before when nothing is pinned. +/// +[TestClass] +public class NativeSmtpHandlerPinningTests +{ + /// Just enough SMTP with STARTTLS to take one message. + sealed class FakeSmtpServer : IDisposable + { + readonly TcpListener listener = new(IPAddress.Loopback, 0); + readonly X509Certificate2 certificate; + readonly CancellationTokenSource stop = new(); + + public int Port { get; } + public bool PasswordSent { get; private set; } + public int Delivered { get; private set; } + + public FakeSmtpServer(X509Certificate2 certificate) + { + this.certificate = certificate; + listener.Start(); + Port = ((IPEndPoint)listener.LocalEndpoint).Port; + _ = Accept(); + } + + async Task Accept() + { + try + { + while (!stop.IsCancellationRequested) + _ = Serve(await listener.AcceptTcpClientAsync(stop.Token)); + } + catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) { } + } + + async Task Serve(TcpClient client) + { + using var _ = client; + Stream stream = client.GetStream(); + var (reader, writer) = Open(stream); + await writer.WriteLineAsync("220 fake ESMTP"); + try + { + while (await reader.ReadLineAsync() is { } line) + { + var command = line.Split(' ')[0].ToUpperInvariant(); + switch (command) + { + case "EHLO": + await writer.WriteLineAsync(stream is SslStream ? "250-fake\r\n250 AUTH PLAIN" : "250-fake\r\n250 STARTTLS"); + break; + case "STARTTLS": + await writer.WriteLineAsync("220 go ahead"); + var ssl = new SslStream(stream, leaveInnerStreamOpen: true); + await ssl.AuthenticateAsServerAsync(certificate); + stream = ssl; + (reader, writer) = Open(stream); + break; + case "AUTH": + PasswordSent = true; + if (line.Split(' ').Length < 3) + { + await writer.WriteLineAsync("334 "); + await reader.ReadLineAsync(); + } + await writer.WriteLineAsync("235 ok"); + break; + case "DATA": + await writer.WriteLineAsync("354 go"); + while (await reader.ReadLineAsync() is { } data && data != ".") { } + Delivered++; + await writer.WriteLineAsync("250 queued"); + break; + case "QUIT": + await writer.WriteLineAsync("221 bye"); + return; + default: + await writer.WriteLineAsync("250 ok"); + break; + } + } + } + catch (Exception ex) when (ex is IOException or System.Security.Authentication.AuthenticationException) { } + } + + static (StreamReader, StreamWriter) Open(Stream stream) => + (new StreamReader(stream, Encoding.ASCII, false, 1024, leaveOpen: true), + new StreamWriter(stream, Encoding.ASCII, 1024, leaveOpen: true) { NewLine = "\r\n", AutoFlush = true }); + + public void Dispose() + { + stop.Cancel(); + listener.Stop(); + } + } + + static X509Certificate2 SelfSigned() + { + using var key = RSA.Create(2048); + var request = new CertificateRequest("CN=relay.lab.local", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30)); + return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), null); + } + + static NativeSmtpHandler Handler(int port, string? thumbprint) + { + var handler = new NativeSmtpHandler(); + var settings = new Dictionary + { + ["Host"] = "127.0.0.1", + ["Port"] = port.ToString(), + ["Password"] = "relay-password", + ["From"] = "bitween@acme.lab", + ["To"] = "ops@acme.lab", + ["Subject"] = "Order {{ orderId }}", + ["Body"] = "Order {{ orderId }} failed.", + }; + if (thumbprint != null) settings["CertificateThumbprint"] = thumbprint; + handler.InitializeStartupValues(settings); + return handler; + } + + static readonly XchangeFile Payload = new("{\"orderId\":\"MKT-1\"}"); + + [TestMethod] + public async Task A_pinned_certificate_is_trusted_though_self_signed() + { + using var cert = SelfSigned(); + using var server = new FakeSmtpServer(cert); + + await Handler(server.Port, Convert.ToHexString(SHA256.HashData(cert.RawData))).Handle(Payload); + + Assert.IsTrue(server.PasswordSent); + Assert.AreEqual(1, server.Delivered); + } + + [TestMethod] + public async Task Another_certificate_than_the_pinned_one_is_refused_before_anything_is_sent() + { + using var cert = SelfSigned(); + using var server = new FakeSmtpServer(cert); + + await Assert.ThrowsExceptionAsync(() => Handler(server.Port, new string('B', 64)).Handle(Payload)); + + Assert.IsFalse(server.PasswordSent); + Assert.AreEqual(0, server.Delivered); + } + + [TestMethod] + public async Task Without_a_pin_a_self_signed_certificate_is_refused_as_before() + { + using var cert = SelfSigned(); + using var server = new FakeSmtpServer(cert); + + await Assert.ThrowsExceptionAsync(() => Handler(server.Port, null).Handle(Payload)); + + Assert.IsFalse(server.PasswordSent); + } +} diff --git a/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs index ec0528b9..e6090b64 100644 --- a/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs +++ b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs @@ -45,7 +45,7 @@ public void Fill_RendersTheRealAlertPayload() [TestMethod] public void Fill_LeavesTemplateAloneForNonJsonPayload() { - // Normal for a pipeline handler shipping a flat file — there are no fields to substitute. + // Normal for a pipeline handler shipping a flat file - there are no fields to substitute. Assert.AreEqual("Nightly export", NativeSmtpHandler.Fill("Nightly export", "id,name\n1,alpha")); } @@ -140,7 +140,7 @@ public void Certificate_ThatWasRevoked_IsRefused() public void Certificate_RevokedAlongsideAnUncheckableStatus_IsRefused() { // One chain entry can carry several flags at once, so the tolerated ones have to be masked - // out rather than compared — otherwise a revoked certificate rides in on the same entry. + // out rather than compared - otherwise a revoked certificate rides in on the same entry. Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( SslPolicyErrors.RemoteCertificateChainErrors, new[] { X509ChainStatusFlags.Revoked | X509ChainStatusFlags.RevocationStatusUnknown })); diff --git a/SW.Bitween.UnitTests/OracleDdlRefusalTests.cs b/SW.Bitween.UnitTests/OracleDdlRefusalTests.cs new file mode 100644 index 00000000..b08052e4 --- /dev/null +++ b/SW.Bitween.UnitTests/OracleDdlRefusalTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Adapters.Db.Oracle; + +namespace SW.Bitween.UnitTests; + +/// +/// Oracle runs DDL as soon as DBMS_SQL.PARSE sees it, so the statement check refuses DDL before +/// parsing. These pin which first word is read, past the whitespace and comments an operator +/// might put in front of it. +/// +[TestClass] +public class OracleDdlRefusalTests +{ + [DataTestMethod] + [DataRow("drop table orders", "drop")] + [DataRow(" \n\tTRUNCATE TABLE orders", "TRUNCATE")] + [DataRow("-- tidy up\nDROP TABLE orders", "DROP")] + [DataRow("/* why */ grant select on orders to public", "grant")] + [DataRow("select * from orders", "select")] + [DataRow("begin null; end;", "begin")] + [DataRow("", null)] + [DataRow("-- only a comment", null)] + public void The_leading_keyword_is_read_past_whitespace_and_comments(string sql, string expected) => + Assert.AreEqual(expected, OracleDbAdapter.LeadingKeyword(sql)); +} diff --git a/SW.Bitween.UnitTests/ResultWaitTests.cs b/SW.Bitween.UnitTests/ResultWaitTests.cs new file mode 100644 index 00000000..9030b516 --- /dev/null +++ b/SW.Bitween.UnitTests/ResultWaitTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +/// +/// The synchronous gateway wait used to compare its polling step, capped at eight seconds, with the +/// requested wait - so a wait of eight seconds or more never ended. +/// +[TestClass] +public class ResultWaitTests +{ + [DataTestMethod] + [DataRow(null, 300, 120)] + [DataRow(0, 300, 120)] + [DataRow(-5, 300, 120)] + [DataRow(30, 300, 30)] + [DataRow(100000, 300, 300)] + public void A_requested_wait_is_defaulted_and_capped(int? requested, int max, int expected) => + Assert.AreEqual(expected, ResultWait.Clamp(requested, max)); + + [TestMethod] + public async Task A_result_that_never_comes_ends_the_wait_on_time() + { + var clock = Stopwatch.StartNew(); + Assert.IsFalse(await ResultWait.UntilAsync(() => Task.FromResult(false), 2, CancellationToken.None)); + Assert.IsTrue(clock.Elapsed < TimeSpan.FromSeconds(4), $"took {clock.Elapsed}"); + } + + [TestMethod] + public async Task A_result_ends_the_wait_as_soon_as_it_is_there() => + Assert.IsTrue(await ResultWait.UntilAsync(() => Task.FromResult(true), 60, CancellationToken.None)); + + [TestMethod] + public async Task The_wait_ends_when_the_caller_goes_away() + { + using var gone = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + var clock = Stopwatch.StartNew(); + Assert.IsFalse(await ResultWait.UntilAsync(() => Task.FromResult(false), 60, gone.Token)); + Assert.IsTrue(clock.Elapsed < TimeSpan.FromSeconds(3), $"took {clock.Elapsed}"); + } +} diff --git a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs index b3c2c920..7f05acf7 100644 --- a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs +++ b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs @@ -279,7 +279,7 @@ public async Task Evaluator_WrongResultType_GroupSkipped() { var policy = PolicyWith(BadResultGroup("bad", new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists })); var ev = Evaluator(policy); - // Group is for BadResult only — must be skipped for Error + // Group is for BadResult only - must be skipped for Error var decision = await ev.Evaluate(XchangeResultType.Error, "some exception", 0); Assert.IsFalse(decision.ShouldRetry); } @@ -297,7 +297,7 @@ public async Task Evaluator_ContainsMatcher_MatchesBadResultBody() [TestMethod] public async Task Evaluator_ContainsMatcher_MatchesNonJsonBadResultBody() { - // A 4xx body need not be JSON — text matchers are the only way to reach these. + // A 4xx body need not be JSON - text matchers are the only way to reach these. var policy = PolicyWith(BadResultGroup("bad", new ContainsMatcher { Value = "rate limit" })); var ev = Evaluator(policy); var decision = await ev.Evaluate(XchangeResultType.BadResult, "Rate limit exceeded", 0); @@ -316,7 +316,7 @@ public async Task Evaluator_RegexMatcher_MatchesBadResultBody() [TestMethod] public async Task Evaluator_ExceptionTypeMatcher_SkippedForBadResult() { - // Exception type names are meaningless against a response body — stays Error-only. + // Exception type names are meaningless against a response body - stays Error-only. var policy = PolicyWith(BadResultGroup("bad", new ExceptionTypeMatcher { Value = "System.TimeoutException" })); var ev = Evaluator(policy); var decision = await ev.Evaluate(XchangeResultType.BadResult, "System.TimeoutException in body", 0); @@ -372,7 +372,7 @@ public async Task Evaluator_OnlyMatchingGroupFires() Assert.AreEqual("disk", decision.MatchedGroupName); } - // ─── Evaluator: budget — MaxAttemptsPerError ───────────────────────────────── + // ─── Evaluator: budget - MaxAttemptsPerError ───────────────────────────────── [TestMethod] public async Task Evaluator_MaxAttemptsPerError_BlocksAfterCap() @@ -385,7 +385,7 @@ public async Task Evaluator_MaxAttemptsPerError_BlocksAfterCap() Assert.IsFalse((await ev.Evaluate(XchangeResultType.Error, "err", 2)).ShouldRetry); // cap = 2 } - // ─── Evaluator: budget — MaxAttemptsTotal ──────────────────────────────────── + // ─── Evaluator: budget - MaxAttemptsTotal ──────────────────────────────────── [TestMethod] public async Task Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() diff --git a/SW.Bitween.UnitTests/RunMapperEnrichmentTests.cs b/SW.Bitween.UnitTests/RunMapperEnrichmentTests.cs index 90562a4b..2ed5ab67 100644 --- a/SW.Bitween.UnitTests/RunMapperEnrichmentTests.cs +++ b/SW.Bitween.UnitTests/RunMapperEnrichmentTests.cs @@ -11,7 +11,7 @@ private static JObject TryParseAsJObject(string data) => JToken.Parse(data) as JObject; [TestMethod] - [Description("Receiver returns a plain JSON string — should not throw and should skip enrichment")] + [Description("Receiver returns a plain JSON string - should not throw and should skip enrichment")] public void WhenDataIsJsonString_ShouldNotThrow_AndReturnNull() { var data = "\"this is a plain string response from the receiver\""; @@ -22,7 +22,7 @@ public void WhenDataIsJsonString_ShouldNotThrow_AndReturnNull() } [TestMethod] - [Description("Receiver returns a JSON object — enrichment should proceed normally")] + [Description("Receiver returns a JSON object - enrichment should proceed normally")] public void WhenDataIsJsonObject_ShouldReturnJObject() { var data = "{\"orderId\":\"123\",\"status\":\"pending\"}"; @@ -34,7 +34,7 @@ public void WhenDataIsJsonObject_ShouldReturnJObject() } [TestMethod] - [Description("Receiver returns a JSON array — should not throw and should skip enrichment")] + [Description("Receiver returns a JSON array - should not throw and should skip enrichment")] public void WhenDataIsJsonArray_ShouldNotThrow_AndReturnNull() { var data = "[\"item1\",\"item2\"]"; @@ -45,7 +45,7 @@ public void WhenDataIsJsonArray_ShouldNotThrow_AndReturnNull() } [TestMethod] - [Description("Large JSON string payload (like the one that triggered the original error) — should not throw")] + [Description("Large JSON string payload (like the one that triggered the original error) - should not throw")] public void WhenDataIsLargeJsonString_ShouldNotThrow_AndReturnNull() { // Simulate the case from the error: a JSON-encoded string with ~200k chars diff --git a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj index a3e0d7e7..86fc73aa 100644 --- a/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj +++ b/SW.Bitween.UnitTests/SW.Bitween.UnitTests.csproj @@ -12,8 +12,6 @@ - - @@ -23,7 +21,9 @@ + + @@ -31,13 +31,15 @@ - + + database does - so it is provable without a container per PostgreSQL release. --> + + diff --git a/SW.Bitween.UnitTests/ScheduledXchangeServiceTests.cs b/SW.Bitween.UnitTests/ScheduledXchangeServiceTests.cs deleted file mode 100644 index 88fc3296..00000000 --- a/SW.Bitween.UnitTests/ScheduledXchangeServiceTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -//using Microsoft.AspNetCore; -//using Microsoft.AspNetCore.Hosting; -//using Microsoft.AspNetCore.TestHost; -//using Microsoft.Data.Sqlite; -//using Microsoft.EntityFrameworkCore; -//using Microsoft.Extensions.Configuration; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.VisualStudio.TestTools.UnitTesting; -//using SW.PrimitiveTypes; -//using System; -//using System.Collections.Generic; -//using System.IO; -//using System.Text; -//using System.Threading.Tasks; - -//namespace SW.Bitween.UnitTests -//{ - -// [TestClass] -// public class ScheduledXchangeServiceTests -// { - -// static TestServer server; - -// [ClassInitialize] -// public static void ClassInitialize(TestContext tcontext) -// { -// server = new TestServer(WebHost.CreateDefaultBuilder() -// .UseEnvironment("Development") -// .UseStartup()); -// } - - -// [TestMethod] -// public async Task TestRun1() -// { -// var sxsvc = server.Services.GetService(); -// var xf = new XchangeFile(File.ReadAllText("./sampledoc.json")); -// using var scope = server.Services.CreateScope(); -// var xsvc = scope.ServiceProvider.GetService(); -// for (var i=0; i<10; i++) -// { -// await xsvc.Submit(1, xf); -// } - -// for (var i = 0; i < 10; i++) -// { -// await xsvc.Submit(2, xf); -// } - -// await sxsvc.Run(DateTime.UtcNow.AddDays(1)); -// //Assert.IsNotNull(settings); -// } - - -// } -//} diff --git a/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs b/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs index 2ce3363f..7dd7be30 100644 --- a/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs +++ b/SW.Bitween.UnitTests/ScribanGeneratorParityTests.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.UnitTests; /// for the corresponding mapping config (frozen in scribanGeneratorOutput.test.ts snapshots). /// /// If a test here fails it means ScribanJsonHelper.Render cannot execute a template -/// that the UI generator produces — a production bug. +/// that the UI generator produces - a production bug. /// If the TypeScript snapshot changes, the template constant here must be updated to match. /// [TestClass] @@ -152,7 +152,7 @@ public void Parity_GlobalSetKey_MissingGlobals_ReturnsNull() // ── Global set id with characters invalid in a bare identifier ───────────── // Config: { target: 'gval', source: '', globalSetId: 'global-value-name', globalKey: 'baseUrl' } // Regression: the old `__globals__?.global-value-name["baseUrl"]` shape tokenized the - // hyphens as subtraction and threw "Object `name` is null" — this is the exact bug report. + // hyphens as subtraction and threw "Object `name` is null" - this is the exact bug report. [TestMethod] public void Parity_GlobalSetKey_HyphenatedSetId_RendersCorrectly() @@ -167,7 +167,7 @@ public void Parity_GlobalSetKey_HyphenatedSetId_RendersCorrectly() Assert.AreEqual("https://example.test", result["gval"]?.ToString()); } - // ── Lookup — null fallback ───────────────────────────────────────────────── + // ── Lookup - null fallback ───────────────────────────────────────────────── // Config: { target: 'category', source: 'Cat', lookupDictionary: { entries:[{A→Alpha}], fallback:'null' } } [TestMethod] @@ -194,7 +194,7 @@ public void Parity_LookupNullFallback_MissReturnsNull() Assert.AreEqual(JTokenType.Null, result["category"]?.Type); } - // ── Lookup — custom fallback ─────────────────────────────────────────────── + // ── Lookup - custom fallback ─────────────────────────────────────────────── // Config: { fallback:'custom', fallbackValue:'Unknown' } [TestMethod] @@ -386,7 +386,7 @@ public void Parity_TransformWithNumberTarget_EmitsTypedExpression() public void Parity_IsRootSource_FieldInsideArrayLoopReadsFromRoot() { // generateScriban: ArrayMapping with one item field (item.Sku) and one - // isRootSource field (OrderId — no alias prefix, reads from root object) + // isRootSource field (OrderId - no alias prefix, reads from root object) const string template = """ { "lines": [ diff --git a/SW.Bitween.UnitTests/ScribanJsonHelperRootArrayTests.cs b/SW.Bitween.UnitTests/ScribanJsonHelperRootArrayTests.cs index 44c19a4a..04eb5f83 100644 --- a/SW.Bitween.UnitTests/ScribanJsonHelperRootArrayTests.cs +++ b/SW.Bitween.UnitTests/ScribanJsonHelperRootArrayTests.cs @@ -18,7 +18,7 @@ public class ScribanJsonHelperRootArrayTests [TestMethod] public void RootArrayInput_AccessFirstItemField_DirectReference() { - // Hoisted top-level keys — direct field access without index + // Hoisted top-level keys - direct field access without index var input = "[{\"OrderId\":1,\"OrderNumber\":\"ORD-001\"}]"; var template = "{ \"id\": {{ OrderId | json }}, \"ref\": {{ OrderNumber | json }} }"; @@ -93,7 +93,7 @@ public void RootArrayInput_AllFourModes_SourceFixedPartnerGlobal() "[{\"OrderId\":7,\"Status\":\"A\"}," + "{\"OrderId\":8,\"Status\":\"B\"}]"; - // Embed __partner__ / __globals__ via hoisted fields — simulate injection + // Embed __partner__ / __globals__ via hoisted fields - simulate injection var inputWithExtras = "[{\"OrderId\":7,\"Status\":\"A\",\"__partner__\":{\"key\":\"PK\"},\"__globals__\":{\"S\":{\"K\":\"GV\"}}}]"; diff --git a/SW.Bitween.UnitTests/ScribanRenderLimitTests.cs b/SW.Bitween.UnitTests/ScribanRenderLimitTests.cs new file mode 100644 index 00000000..d05b0fbe --- /dev/null +++ b/SW.Bitween.UnitTests/ScribanRenderLimitTests.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters.JsonMapper; + +namespace SW.Bitween.UnitTests; + +/// +/// The preview renders whatever template the caller sends, inside the API process. Scriban caps +/// each loop statement's iterations itself; the time limit covers whatever runs long anyway. +/// +[TestClass] +public class ScribanRenderLimitTests +{ + [TestMethod] + public void A_render_past_its_time_limit_is_stopped() + { + var saved = ScribanJsonHelper.RenderTimeout; + ScribanJsonHelper.RenderTimeout = TimeSpan.Zero; + try + { + var ex = Assert.ThrowsException(() => + ScribanJsonHelper.Render("{{ for i in 1..500 }}{{ i }}{{ end }}", "{}")); + + StringAssert.Contains(ex.Message, "stopped"); + } + finally + { + ScribanJsonHelper.RenderTimeout = saved; + } + } + + [TestMethod] + public void Nested_loops_are_capped_by_Scriban_itself() => + Assert.ThrowsException(() => ScribanJsonHelper.Render( + "{{ for a in 1..999 }}{{ for b in 1..999 }}{{ b }}{{ end }}{{ end }}", "{}")); +} diff --git a/SW.Bitween.UnitTests/ScribanTrailingCommaTests.cs b/SW.Bitween.UnitTests/ScribanTrailingCommaTests.cs new file mode 100644 index 00000000..54051cca --- /dev/null +++ b/SW.Bitween.UnitTests/ScribanTrailingCommaTests.cs @@ -0,0 +1,17 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.NativeAdapters.JsonMapper; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class ScribanTrailingCommaTests +{ + [DataTestMethod] + [DataRow("{\"a\": 1,}", "{\"a\": 1}")] + [DataRow("[1, 2,\n ]", "[1, 2\n ]")] + [DataRow("{\"note\": \"a, }\"}", "{\"note\": \"a, }\"}")] + [DataRow("{\"q\": \"say \\\"x, ]\\\"\",}", "{\"q\": \"say \\\"x, ]\\\"\"}")] + [DataRow("{\"a\": [1,], \"b\": 2,}", "{\"a\": [1], \"b\": 2}")] + public void Only_trailing_commas_outside_strings_are_removed(string input, string expected) => + Assert.AreEqual(expected, ScribanJsonHelper.StripTrailingCommas(input)); +} diff --git a/SW.Bitween.UnitTests/SearchServiceTests.cs b/SW.Bitween.UnitTests/SearchServiceTests.cs deleted file mode 100644 index 28b04dc5..00000000 --- a/SW.Bitween.UnitTests/SearchServiceTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -//using Microsoft.AspNetCore.Hosting; -//using Microsoft.AspNetCore.TestHost; -//using Microsoft.VisualStudio.TestTools.UnitTesting; -//using Microsoft.Extensions.DependencyInjection; -//using System; -//using System.Collections.Generic; -//using System.Text; - -//using SW.PrimitiveTypes; -//using System.Threading.Tasks; -//using SW.Bitween.Domain; -//using SW.EfCoreExtensions; -//using Microsoft.EntityFrameworkCore; -//using Microsoft.AspNetCore; - -//namespace SW.Bitween.UnitTests -//{ - -// [TestClass] -// public class SearchServiceTests -// { -// static TestServer server; - -// [ClassInitialize] -// public static void ClassInitialize(TestContext tcontext) -// { -// server = new TestServer(WebHost.CreateDefaultBuilder() -// .UseEnvironment("Development") -// .UseStartup()); -// } - -// [ClassCleanup] -// public static void ClassCleanup() -// { -// } - -// //[TestMethod] -// //public async Task TestAdapterSearch() -// //{ -// // using var scope = server.Services.CreateScope(); -// // var ctxt = scope.ServiceProvider.GetService(); -// // var Conditions = new List { new SearchyCondition(new List { new SearchyFilter("Id", SearchyRule.EqualsTo, 1) }) }; -// // //var x = await ctxt.Set().Search(Conditions).ToListAsync(); -// //} - -// [TestMethod] -// public async Task TestXchangeSearch() -// { -// using var scope = server.Services.CreateScope(); - -// var ctxt = scope.ServiceProvider.GetService(); - -// //var d = new Document(1, "Doc1"); -// //ctxt.Add(d); - -// //var k = new AccessKeySet("test", "1234", "5678"); - -// //ctxt.Add(k); - -// var s = new Subscription("Test", 1) -// { -// //KeySetId = 1 -// }; -// ctxt.Add(s); -// ctxt.SaveChanges(); - -// var xsvc = scope.ServiceProvider.GetService(); - -// xsvc.Submit(s.Id, new XchangeFile("test")).Wait(); - - - -// //var searchSvc = services.GetService>(); -// //SearchyRequest searchyRequest = new SearchyRequest -// //{ -// // Conditions = new List { new SearchyCondition(new List { new SearchyFilter("Id", SearchyRule.EqualsTo, 1) }) } -// //}; -// //var x = await searchSvc.Search(searchyRequest); - -// //var ctxt = services.GetService(); -// var Conditions = new List { new SearchyCondition(new List { new SearchyFilter("Id", SearchyRule.EqualsTo, 1) }) }; -// var x = await ctxt.Set().Search(Conditions).ToListAsync(); - -// } -// } -//} diff --git a/SW.Bitween.UnitTests/SecretColumnCipherTests.cs b/SW.Bitween.UnitTests/SecretColumnCipherTests.cs new file mode 100644 index 00000000..993d4187 --- /dev/null +++ b/SW.Bitween.UnitTests/SecretColumnCipherTests.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class SecretColumnCipherTests +{ + [TestCleanup] + public void Reset() => SecretColumnCipher.Configure(null); + + [TestMethod] + public void Without_a_key_values_are_stored_as_they_are() + { + SecretColumnCipher.Configure(null); + Assert.AreEqual("{\"Password\":\"p\"}", SecretColumnCipher.Protect("{\"Password\":\"p\"}")); + } + + [TestMethod] + public void A_value_round_trips_and_is_not_stored_in_clear() + { + SecretColumnCipher.Configure("a passphrase for the tests"); + var stored = SecretColumnCipher.Protect("{\"Password\":\"hunter2\"}"); + + StringAssert.StartsWith(stored, SecretColumnCipher.Prefix); + Assert.IsFalse(stored.Contains("hunter2")); + Assert.AreEqual("{\"Password\":\"hunter2\"}", SecretColumnCipher.Unprotect(stored)); + } + + [TestMethod] + public void Plaintext_written_before_encryption_still_reads() + { + SecretColumnCipher.Configure("a passphrase for the tests"); + Assert.AreEqual("{\"a\":\"b\"}", SecretColumnCipher.Unprotect("{\"a\":\"b\"}")); + } + + [TestMethod] + public void A_value_written_with_the_previous_key_reads_after_the_key_changes() + { + SecretColumnCipher.Configure("the old key"); + var stored = SecretColumnCipher.Protect("secret"); + + SecretColumnCipher.Configure("the new key", previousPassphrase: "the old key"); + Assert.AreEqual("secret", SecretColumnCipher.Unprotect(stored)); + } + + [TestMethod] + public void A_value_written_with_an_unknown_key_says_so() + { + SecretColumnCipher.Configure("the old key"); + var stored = SecretColumnCipher.Protect("secret"); + + SecretColumnCipher.Configure("a different key"); + var ex = Assert.ThrowsException(() => SecretColumnCipher.Unprotect(stored)); + StringAssert.Contains(ex.Message, "PreviousSettingsEncryptionKey"); + } +} diff --git a/SW.Bitween.UnitTests/SecretKeepingTests.cs b/SW.Bitween.UnitTests/SecretKeepingTests.cs new file mode 100644 index 00000000..6fc99652 --- /dev/null +++ b/SW.Bitween.UnitTests/SecretKeepingTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SW.Bitween.UnitTests; + +/// +/// A masked secret sent back means "keep what is stored" - but not once the adapter, or where it +/// connects, has changed, or an editor who never saw the password could send it to their own server. +/// +[TestClass] +public class SecretKeepingTests +{ + private static readonly Dictionary Stored = new() + { + ["Url"] = "https://partner.example/api", ["Password"] = "secret", ["Subject"] = "Orders", + }; + + private static Dictionary Incoming(string url = "https://partner.example/api", + string subject = "Orders") => new() + { + ["Url"] = url, ["Password"] = AdapterSecretProperties.Sentinel, ["Subject"] = subject, + }; + + [TestMethod] + public void An_unrelated_edit_keeps_the_secret() => + Assert.IsTrue(AdapterSecretProperties.MayKeepStoredSecrets("http", "http", Stored, Incoming(subject: "Invoices"))); + + [TestMethod] + public void Pointing_the_adapter_elsewhere_drops_it() => + Assert.IsFalse(AdapterSecretProperties.MayKeepStoredSecrets("http", "http", Stored, Incoming(url: "https://attacker.example"))); + + [TestMethod] + public void Switching_adapters_drops_it() => + Assert.IsFalse(AdapterSecretProperties.MayKeepStoredSecrets("http", "smtp", Stored, Incoming())); + + [TestMethod] + public void Removing_the_destination_drops_it() + { + var incoming = Incoming(); + incoming.Remove("Url"); + Assert.IsFalse(AdapterSecretProperties.MayKeepStoredSecrets("http", "http", Stored, incoming)); + } + + [DataTestMethod] + [DataRow("Url", true)] [DataRow("LoginUrl", true)] [DataRow("ServiceUrl", true)] [DataRow("Host", true)] + [DataRow("SmtpServer", true)] [DataRow("Port", true)] [DataRow("ConnectionString", true)] + [DataRow("Subject", false)] [DataRow("Password", false)] [DataRow("BatchSize", false)] + public void Destinations_are_recognised_by_name(string key, bool destination) => + Assert.AreEqual(destination, AdapterSecretProperties.IsDestination(key)); + + [TestMethod] + public void When_it_may_not_be_kept_the_sentinel_restores_nothing() + { + var merged = AdapterSecretProperties.Merge(Stored, Incoming(url: "https://attacker.example"), keepStored: false); + Assert.IsFalse(merged.ContainsKey("Password")); + } +} diff --git a/SW.Bitween.UnitTests/SecretStartupDefaultsTests.cs b/SW.Bitween.UnitTests/SecretStartupDefaultsTests.cs index 1666f617..1f77e6a5 100644 --- a/SW.Bitween.UnitTests/SecretStartupDefaultsTests.cs +++ b/SW.Bitween.UnitTests/SecretStartupDefaultsTests.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.UnitTests; /// /// An adapter's description goes to the browser to draw its form, and a secret property's default -/// went with it — shown as the field's placeholder. One adapter shipped a production storage key +/// went with it - shown as the field's placeholder. One adapter shipped a production storage key /// that way. /// [TestClass] diff --git a/SW.Bitween.UnitTests/ServiceTests.cs b/SW.Bitween.UnitTests/ServiceTests.cs deleted file mode 100644 index df42bd9c..00000000 --- a/SW.Bitween.UnitTests/ServiceTests.cs +++ /dev/null @@ -1,262 +0,0 @@ -//using Microsoft.AspNetCore; -//using Microsoft.AspNetCore.Hosting; -//using Microsoft.AspNetCore.TestHost; -//using Microsoft.Data.Sqlite; -//using Microsoft.EntityFrameworkCore; -//using Microsoft.Extensions.Configuration; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Hosting; -//using Microsoft.VisualStudio.TestTools.UnitTesting; -//using Newtonsoft.Json.Linq; -//using SW.EfCoreExtensions; -//using SW.Bitween; -//using SW.Bitween.Domain; -//using SW.Bitween.Model; -//using SW.PrimitiveTypes; -//using System; -//using System.Collections.Generic; -//using System.IO; -//using System.Linq; -//using System.Threading.Tasks; - -//namespace SW.Bitween.UnitTests -//{ -// [TestClass] -// public class ServiceTests -// { -// static TestServer server; - -// [ClassInitialize] -// public static void ClassInitialize(TestContext tcontext) -// { -// server = new TestServer(WebHost.CreateDefaultBuilder() -// .UseEnvironment("Development") -// .UseStartup()); -// } - -// [ClassCleanup] -// public static void ClassCleanup() -// { -// server.Dispose(); -// } - -// [TestMethod] -// public void TestSettings() -// { -// var settings = server.Services.GetService(); - -// Assert.IsNotNull(settings); -// } - -// [TestMethod] -// public void TestFilterDataService() -// { -// var fs = server.Services.GetService(); -// var xf = new XchangeFile(File.ReadAllText("./sampledoc.json")); -// var res =fs.Filter(1, xf).Count(); -// Assert.AreNotEqual(0, fs.Filter(1, xf).Count()); -// } - -// [TestMethod] -// public void TestFilterDataServicePartner() -// { -// var fs = server.Services.GetService(); -// var xf = new XchangeFile(File.ReadAllText("./sampledoc2.json")); -// var res = fs.Filter(3, xf).Count(); -// Assert.AreNotEqual(0, fs.Filter(3, xf).Count()); - - -// } -// [TestMethod] -// public void TestFilterDataServiceWithNoPromotion() -// { -// var fs = server.Services.GetService(); -// var xf = new XchangeFile(File.ReadAllText("./sampledoc.json")); - -// Assert.AreNotEqual(0, fs.Filter(22, xf).Count()); - -// } -// [TestMethod] -// public async Task RetryXchange() -// { -// using var scope = server.Services.CreateScope(); -// var xchangeService = scope.ServiceProvider.GetService(); -// //await xchangeService.Retry(23); - -// } - - -// //[TestMethod] -// //public async Task TestMailBoxService() -// //{ -// // using var scope = server.Services.CreateScope(); -// // var ctxt = scope.ServiceProvider.GetService(); - -// // //var d = new Document(1, "Doc1"); -// // //ctxt.Add(d); - -// // var k = new AccessKeySet("test", "1234", "5678"); -// // await ctxt.AddAsync(k); - -// // var s = new Subscriber("Test", 1) -// // { -// // KeySetId = 1 -// // }; -// // await ctxt.AddAsync(s); -// // await ctxt.SaveChangesAsync(); - -// // var xsvc = scope.ServiceProvider.GetService(); - -// // var xchangeid= await xsvc.Submit(s.Id, new XchangeFile("test")); - -// // //var msvc = services.GetService(); -// // var cred = new SubscriptionCredential(s.Id, "1234"); -// // await ValidateSubscriber(cred, ctxt); - -// // var xs = await ctxt.ListAsync(new XchangesByDateAndDeliveryFlag(cred.SubscriberId, DateTime.UtcNow.AddDays(-1),null)); - -// // var arr =xs.Select(e => e.Id).ToArray(); -// // //var arr = ctxt.Set().Listas(cred, DateTime.UtcNow.AddDays(-1)); - -// // //var doc = await msvc.Get(cred, xchangeid); -// //} - -// //async Task ValidateSubscriber(SubscriptionCredential credential, DbContext dbContext) -// //{ -// // var sub = await dbContext.FindAsync(credential.SubscriberId); -// // if (sub == null) throw new BitweenException(); -// // if (sub.KeySetId == 0) throw new BitweenException(); - -// // var ks = await dbContext.ListAsync(new AccessKeySetyByKey(sub.KeySetId, credential.Key)); - -// // if (ks.Count != 1) throw new BitweenException(); -// //} - -// [TestMethod] -// public async Task TestSubmitXchangeScheduledMonthly() -// { -// using var scope = server.Services.CreateScope(); -// var ctxt = scope.ServiceProvider.GetService(); - -// //var d = new Document(1, "Doc1"); -// //ctxt.Add(d); - - -// //monthly -// var s = new Subscription("Test", 1); -// var scheds = new List(); -// //scheds.Add("14.15:30:00"); -// //scheds.Add("17.15:30:00"); -// //scheds.Add("0.15:30:00"); - -// //var schedsarr = scheds.ToArray(); -// //var timeSpans = schedsarr.Select(e => TimeSpan.Parse(e)).ToArray(); -// var sched = new Schedule(Recurrence.Monthly, TimeSpan.Parse("0.15:30:00")); -// s.Schedules.Add(sched); -// ctxt.Add(s); - - - -// await ctxt.SaveChangesAsync(); - -// var xsvc = scope.ServiceProvider.GetService(); - -// await xsvc.Submit(s.Id, new XchangeFile("test")); - - - - -// } - -// [TestMethod] -// public async Task TestSubmitXchangeScheduledWeekly() -// { -// using var scope = server.Services.CreateScope(); -// var ctxt = scope.ServiceProvider.GetService(); - -// //weekly -// var s = new Subscription("Test", 1); -// var scheds = new List(); - -// //scheds.Add("6.15:30:00"); -// //scheds.Add("4.15:30:00"); -// //scheds.Add("5.15:30:00"); -// //scheds.Add("0.15:30:00"); -// //var schedsarr = scheds.ToArray(); -// //var timeSpans = schedsarr.Select(e => TimeSpan.Parse(e)).ToArray(); -// var sched = new Schedule(Recurrence.Weekly, TimeSpan.Parse("4.15:30:00")); -// s.Schedules.Add(sched); -// await ctxt.AddAsync(s); - -// await ctxt.SaveChangesAsync(); - -// var xsvc = scope.ServiceProvider.GetService(); - -// await xsvc.Submit(s.Id, new XchangeFile("test")); -// } -// [TestMethod] -// public async Task TestJsonAggregation() -// { -// JArray jArray = null; -// jArray = new JArray(); - -// var firstjson = "[{\"payload\":{\"number\":{\"scheme\":\"default\",\"value\":\"\"},\"references\":[{\"type\":\"reftype\",\"value\":\"refval\"}],\"product\":\"STD\",\"ship_date\":\"2019-01-25T05:44:12.251Z\",\"shipper\":{\"name\":\"string\",\"phones\":[\"787988\"],\"emails\":[\"string\"],\"address\":{\"country\":\"US\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"consignee\":{\"name\":\"string\",\"phones\":[\"4234234\"],\"emails\":[\"string\"],\"address\":{\"country\":\"AE\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"account\":{\"number\":\"0001\",\"entity\":\"DXB\"},\"weight\":{\"value\":1,\"unit\":\"gm\"},\"charge_weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"items\":[{\"weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"customs_value\":{\"amount\":1,\"currency\":\"USD\"},\"origin_country\":\"US\",\"description\":\"string\",\"hs_code\":\"string\",\"quantity\":1}]}},{\"payload\":{\"number\":{\"scheme\":\"default\",\"value\":\"\"},\"references\":[{\"type\":\"reftype\",\"value\":\"refval\"}],\"product\":\"STD\",\"ship_date\":\"2019-09-25T05:44:12.251Z\",\"shipper\":{\"name\":\"string\",\"phones\":[\"0797439948\"],\"emails\":[\"string\"],\"address\":{\"country\":\"US\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"consignee\":{\"name\":\"string\",\"phones\":[\"0797439948\"],\"emails\":[\"string\"],\"address\":{\"country\":\"AE\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"account\":{\"number\":\"0001\",\"entity\":\"DXB\"},\"weight\":{\"value\":1,\"unit\":\"gm\"},\"charge_weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"items\":[{\"weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"customs_value\":{\"amount\":1,\"currency\":\"USD\"},\"origin_country\":\"US\",\"description\":\"string\",\"hs_code\":\"string\",\"quantity\":1}]}}]"; -// var secjson = "{\"payload\":{\"number\":{\"scheme\":\"default\",\"value\":\"\"},\"references\":[{\"type\":\"reftype\",\"value\":\"refval\"}],\"product\":\"STD\",\"ship_date\":\"2019-01-25T05:44:12.251Z\",\"shipper\":{\"name\":\"string\",\"phones\":[\"787988\"],\"emails\":[\"string\"],\"address\":{\"country\":\"US\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"consignee\":{\"name\":\"string\",\"phones\":[\"4234234\"],\"emails\":[\"string\"],\"address\":{\"country\":\"AE\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"account\":{\"number\":\"0001\",\"entity\":\"DXB\"},\"weight\":{\"value\":1,\"unit\":\"gm\"},\"charge_weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"items\":[{\"weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"customs_value\":{\"amount\":1,\"currency\":\"USD\"},\"origin_country\":\"US\",\"description\":\"string\",\"hs_code\":\"string\",\"quantity\":1}]}}"; -// var thrdjson = "{\"payload\":{\"number\":{\"scheme\":\"default\",\"value\":\"\"},\"references\":[{\"type\":\"reftype\",\"value\":\"refval\"}],\"product\":\"STD\",\"ship_date\":\"2019-01-25T05:44:12.251Z\",\"shipper\":{\"name\":\"string\",\"phones\":[\"0797439948\"],\"emails\":[\"string\"],\"address\":{\"country\":\"US\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"consignee\":{\"name\":\"string\",\"phones\":[\"0797439948\"],\"emails\":[\"string\"],\"address\":{\"country\":\"AE\",\"city\":\"string\",\"state\":\"string\",\"street\":[\"string\"],\"post_code\":\"string\"}},\"account\":{\"number\":\"0001\",\"entity\":\"DXB\"},\"weight\":{\"value\":1,\"unit\":\"gm\"},\"charge_weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"items\":[{\"weight\":{\"value\":1,\"unit\":\"gm\"},\"dimensions\":{\"length\":0,\"width\":0,\"height\":0,\"unit\":\"cm\"},\"customs_value\":{\"amount\":1,\"currency\":\"USD\"},\"origin_country\":\"US\",\"description\":\"string\",\"hs_code\":\"string\",\"quantity\":1}]}}"; - -// JToken xf = JToken.Parse(secjson); -// JToken xf2 = JToken.Parse(thrdjson); -// JToken xf5 = JToken.Parse(firstjson); - -// AddTokensToArray(jArray, xf); -// AddTokensToArray(jArray, xf2); -// AddTokensToArray(jArray, xf5); - - -// var aggr = jArray.ToString(); -// using var scope = server.Services.CreateScope(); - -// var ctxt = scope.ServiceProvider.GetService(); - -// var xchange = new Xchange(1, 1, new XchangeFile( firstjson)); -// var xchange2 = new Xchange(1, 1, new XchangeFile(secjson)); -// var xchange3 = new Xchange(1, 1, new XchangeFile(thrdjson)); -// //xchange.DeliverOn = DateTime.UtcNow.AddDays(-1); -// //xchange2.DeliverOn = DateTime.UtcNow.AddDays(-1); -// //xchange3.DeliverOn = DateTime.UtcNow.AddDays(-1); - -// var s = new Subscription("Test", 1); -// s.Aggregate = true; -// await ctxt.AddAsync(s); -// await ctxt.AddAsync(xchange); -// await ctxt.AddAsync(xchange2); -// await ctxt.AddAsync(xchange3); -// await ctxt.SaveChangesAsync(); -// //var xsvc = services.GetService(); -// //var xsvc1 = services.GetService(); -// //xsvc.Run(new object()).Wait(); - -// } - -// void AddTokensToArray(JArray jArray, JToken jtoken) -// { -// if (jArray != null) -// { -// if (jtoken is JArray) -// { -// foreach (JToken token in jtoken) -// { -// jArray.Add(token); -// } -// } -// else if (jtoken is JObject) -// { -// jArray.Add(jtoken); -// } -// } - -// } - - -// } -//} diff --git a/SW.Bitween.UnitTests/SettingsCatalogTests.cs b/SW.Bitween.UnitTests/SettingsCatalogTests.cs index 8a93bc8c..d541fd8e 100644 --- a/SW.Bitween.UnitTests/SettingsCatalogTests.cs +++ b/SW.Bitween.UnitTests/SettingsCatalogTests.cs @@ -5,8 +5,8 @@ namespace SW.Bitween.UnitTests; /// -/// The settings page renders whatever the catalog says, so what an administrator is offered — which -/// sections, which rows are edits and which are only shown — is decided here rather than in the UI. +/// The settings page renders whatever the catalog says, so what an administrator is offered - which +/// sections, which rows are edits and which are only shown - is decided here rather than in the UI. /// [TestClass] public class SettingsCatalogTests @@ -66,7 +66,7 @@ public void Microsoft_only_sign_in_is_an_editable_setting_not_an_environment_val { var definition = SettingsCatalog.Find("Bitween.DisableEmailPasswordLogin"); - // It applies per request — the Login handler and the config endpoint both read it live — so + // It applies per request - the Login handler and the config endpoint both read it live - so // it belongs in the catalog as an edit rather than a read-only environment row. Assert.AreEqual("Single sign-on (Microsoft)", definition.Section); Assert.AreEqual("Microsoft sign-in only", definition.Label); diff --git a/SW.Bitween.UnitTests/SettingsProtectorTests.cs b/SW.Bitween.UnitTests/SettingsProtectorTests.cs index f154fc30..06cb36a0 100644 --- a/SW.Bitween.UnitTests/SettingsProtectorTests.cs +++ b/SW.Bitween.UnitTests/SettingsProtectorTests.cs @@ -9,7 +9,7 @@ namespace SW.Bitween.UnitTests; public class SettingsProtectorTests { /// - /// Shaped like a Rebex license key — the longest secret the catalog holds — but deliberately + /// Shaped like a Rebex license key - the longest secret the catalog holds - but deliberately /// not one. Nothing here depends on the contents, only on the round trip. /// private const string Secret = "==NOT-A-REAL-KEY-0123456789abcdefghijklmnop=="; @@ -68,7 +68,7 @@ public void Tampering_is_detected_rather_than_decrypted() // Flip a bit in the payload rather than in a base64 character. Base64 packs three // bytes into four characters, so the last character of a value carries spare bits - // that decode to nothing — changing it left the bytes identical about one run in + // that decode to nothing - changing it left the bytes identical about one run in // six, and the test then failed for having tampered with nothing. // // The middle byte, so this holds whatever the layout is: salt, nonce, ciphertext diff --git a/SW.Bitween.UnitTests/StatementComposerTests.cs b/SW.Bitween.UnitTests/StatementComposerTests.cs index e8f42717..c19d1615 100644 --- a/SW.Bitween.UnitTests/StatementComposerTests.cs +++ b/SW.Bitween.UnitTests/StatementComposerTests.cs @@ -95,7 +95,7 @@ public void Sql_with_quotes_and_newlines_survives_the_round_trip() /// /// Stable ordering. The supervisor fingerprints startup values to decide whether an adapter /// needs restarting, so an order that varied between reconciles would recycle a healthy - /// database connection every thirty seconds — and nothing would say why. + /// database connection every thirty seconds - and nothing would say why. /// [TestMethod] public void Composition_is_stable_whatever_order_the_rows_arrive_in() @@ -122,7 +122,7 @@ public void Composition_is_stable_whatever_order_the_rows_arrive_in() /// /// This is what makes the richer shape safe to introduce: upgrading the host ahead of the /// adapters cannot change the value handed to an adapter for any statement it already runs, - /// and the supervisor's fingerprint of that value does not move either — so no healthy + /// and the supervisor's fingerprint of that value does not move either - so no healthy /// connection is recycled by the deployment. /// [TestMethod] diff --git a/SW.Bitween.UnitTests/StatementRegistryTests.cs b/SW.Bitween.UnitTests/StatementRegistryTests.cs index a07d852e..d18219f5 100644 --- a/SW.Bitween.UnitTests/StatementRegistryTests.cs +++ b/SW.Bitween.UnitTests/StatementRegistryTests.cs @@ -60,7 +60,7 @@ public void Raw_sql_runs_when_the_data_source_opts_in() } /// - /// A name always wins over sql, even with ad-hoc allowed — otherwise a request carrying both + /// A name always wins over sql, even with ad-hoc allowed - otherwise a request carrying both /// would run whichever the implementation happened to check first. /// [TestMethod] @@ -98,7 +98,7 @@ public void No_statements_configured_says_so_rather_than_listing_nothing() } /// - /// Bad JSON fails at startup, where the message can point at the Statements setting — not on + /// Bad JSON fails at startup, where the message can point at the Statements setting - not on /// the first message, as "unknown statement", which sends whoever is debugging it looking at /// the subscription instead of the data source. /// diff --git a/SW.Bitween.UnitTests/TestStartup.cs b/SW.Bitween.UnitTests/TestStartup.cs deleted file mode 100644 index c499d3f8..00000000 --- a/SW.Bitween.UnitTests/TestStartup.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using SW.Bus; -using SW.CqApi; -using SW.Bitween.Domain; -using SW.Bitween.Model; -using SW.PrimitiveTypes; - -namespace SW.Bitween.UnitTests -{ - public class TestStartup - { - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - var BitweenOptions = new BitweenOptions(); - services.AddSingleton(BitweenOptions); - services.AddSingleton(); - services.AddScoped(); - - services.AddCqApi(typeof(BitweenDbContext).Assembly); - - services.AddControllers(). - AddApplicationPart(typeof(CqApiController).Assembly); - services.AddAuthorization(); - services.AddAuthentication(). - AddJwtBearer(); - - services.AddDbContext(builder => - { - var _connection = new SqliteConnection("DataSource=:memory:"); - _connection.Open(); - - builder - .UseSqlite(_connection) - .EnableSensitiveDataLogging(true); - }, - ServiceLifetime.Scoped, - ServiceLifetime.Singleton); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - - app.UseRouting(); - app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/SW.Bitween.UnitTests/TrafficWindowTests.cs b/SW.Bitween.UnitTests/TrafficWindowTests.cs new file mode 100644 index 00000000..9c4797bf --- /dev/null +++ b/SW.Bitween.UnitTests/TrafficWindowTests.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests; + +/// +/// When a subscription that expects traffic counts as quiet. 2026-10-11 is a Sunday; Asia/Riyadh is +/// UTC+3 all year. +/// +[TestClass] +public class TrafficWindowTests +{ + static DateTime Utc(int day, int hour, int minute = 0) => new(2026, 10, day, hour, minute, 0, DateTimeKind.Utc); + + static TrafficWindow Riyadh(int every = 15, string[] days = null, string from = "08:00", string to = "22:00") => + TrafficWindow.Parse(new ExpectTraffic + { + EveryMinutes = every, + Days = days ?? ["Sun", "Mon", "Tue", "Wed", "Thu"], + From = from, + To = to, + TimeZone = "Asia/Riyadh", + }); + + [TestMethod] + public void Inside_the_window_silence_is_counted_back_from_now() + { + // Monday 08:30 in Riyadh. + Assert.AreEqual(Utc(12, 5, 15), Riyadh().QuietUnlessAfter(Utc(12, 5, 30))); + Assert.AreEqual(Utc(12, 5, 0), Riyadh().OpenedAt(Utc(12, 5, 30))); + } + + [TestMethod] + public void A_window_that_just_opened_waits_a_whole_gap_before_it_can_be_quiet() + { + // 08:10: yesterday evening's last order is not a reason to raise ten minutes into the day. + Assert.IsNull(Riyadh().QuietUnlessAfter(Utc(12, 5, 10))); + Assert.AreEqual(Utc(12, 5, 0), Riyadh().QuietUnlessAfter(Utc(12, 5, 15))); + } + + [TestMethod] + public void Outside_the_hours_or_the_days_nothing_is_due() + { + Assert.IsNull(Riyadh().OpenedAt(Utc(12, 4, 59)), "07:59 local"); + Assert.IsNull(Riyadh().OpenedAt(Utc(12, 19, 0)), "22:00 local is closed: To is exclusive"); + Assert.IsNotNull(Riyadh().OpenedAt(Utc(12, 18, 59)), "21:59 local"); + Assert.IsNull(Riyadh().QuietUnlessAfter(Utc(16, 9, 0)), "Friday"); + Assert.IsNull(Riyadh().QuietUnlessAfter(Utc(10, 9, 0)), "Saturday"); + } + + [TestMethod] + public void Days_and_times_are_read_in_the_time_zone() + { + // 20:00 UTC on Monday is 23:00 in Riyadh, closed there but open by UTC. + Assert.IsNull(Riyadh().OpenedAt(Utc(12, 20, 0))); + var utc = TrafficWindow.Parse(new ExpectTraffic { EveryMinutes = 15, From = "08:00", To = "22:00" }); + Assert.AreEqual(Utc(12, 8, 0), utc.OpenedAt(Utc(12, 20, 0))); + + // Sunday 22:00 UTC is already Monday in Riyadh; Monday 22:00 UTC is Tuesday 01:00. + var allDayMonday = TrafficWindow.Parse(new ExpectTraffic { EveryMinutes = 15, Days = ["Mon"], TimeZone = "Asia/Riyadh" }); + Assert.IsNotNull(allDayMonday.OpenedAt(Utc(11, 22, 0))); + Assert.IsNull(allDayMonday.OpenedAt(Utc(12, 22, 0))); + } + + [TestMethod] + public void Whole_days_in_a_row_carry_silence_over_midnight() + { + var days = TrafficWindow.Parse(new ExpectTraffic + { EveryMinutes = 60, Days = ["Sun", "Mon", "Tue", "Wed", "Thu"], TimeZone = "Asia/Riyadh" }); + + // Monday 00:10 local: open since Sunday 00:00 local, so an hour of silence spans midnight. + Assert.AreEqual(Utc(10, 21, 0), days.OpenedAt(Utc(11, 21, 10))); + Assert.AreEqual(Utc(11, 20, 10), days.QuietUnlessAfter(Utc(11, 21, 10))); + + // Sunday 00:30 local: Saturday was closed, so the window is only half an hour old. + Assert.IsNull(days.QuietUnlessAfter(Utc(10, 21, 30))); + + // Friday 00:10 local: Thursday's window ended at midnight. + Assert.IsNull(days.OpenedAt(Utc(15, 21, 10))); + } + + [TestMethod] + public void A_window_with_hours_opens_afresh_each_day() + { + // Tuesday 08:05 local: Monday's window closed at 22:00 and doesn't carry over. + Assert.IsNull(Riyadh().QuietUnlessAfter(Utc(13, 5, 5))); + } + + [TestMethod] + public void Every_day_all_day_never_closes() + { + var always = TrafficWindow.Parse(new ExpectTraffic { EveryMinutes = 30 }); + Assert.AreEqual(DateTime.MinValue, always.OpenedAt(Utc(12, 0, 0))); + Assert.AreEqual(Utc(11, 23, 30), always.QuietUnlessAfter(Utc(12, 0, 0))); + } + + [TestMethod] + public void A_zone_with_summer_time_follows_its_clock() + { + // London leaves summer time on 2026-10-25: 08:00 local is 07:00 UTC before, 08:00 after. + var london = TrafficWindow.Parse(new ExpectTraffic + { EveryMinutes = 15, From = "08:00", To = "17:00", TimeZone = "Europe/London" }); + Assert.AreEqual(new DateTime(2026, 10, 23, 7, 0, 0, DateTimeKind.Utc), + london.OpenedAt(new DateTime(2026, 10, 23, 9, 0, 0, DateTimeKind.Utc))); + Assert.AreEqual(new DateTime(2026, 10, 26, 8, 0, 0, DateTimeKind.Utc), + london.OpenedAt(new DateTime(2026, 10, 26, 9, 0, 0, DateTimeKind.Utc))); + } + + [TestMethod] + public void It_is_stored_in_one_spelling() + { + var stored = TrafficWindow.Normalize(new ExpectTraffic + { + EveryMinutes = 15, Days = ["thu", " Sun", "MON", "Mon"], From = "8:00", To = "22:00", TimeZone = " ", + }); + CollectionAssert.AreEqual(new[] { "Sun", "Mon", "Thu" }, stored.Days.ToArray()); + Assert.AreEqual("08:00", stored.From); + Assert.IsNull(stored.TimeZone); + + Assert.IsNull(TrafficWindow.Normalize(new ExpectTraffic { EveryMinutes = 15, Days = [] }).Days); + Assert.IsNull(TrafficWindow.Normalize(null)); + } + + [TestMethod] + public void It_reads_as_a_sentence() + { + Assert.AreEqual("every 15 minutes, Sun-Thu 08:00-22:00 Asia/Riyadh", Riyadh().Describe()); + Assert.AreEqual("every 30 minutes, Mon, Wed, Fri UTC", + TrafficWindow.Parse(new ExpectTraffic { EveryMinutes = 30, Days = ["Fri", "Mon", "Wed"] }).Describe()); + Assert.AreEqual("every 30 minutes, every day UTC", + TrafficWindow.Parse(new ExpectTraffic { EveryMinutes = 30 }).Describe()); + } + + [DataTestMethod] + [DataRow(4, null, null, null, null, "5 to")] + [DataRow(15, "Funday", null, null, null, "isn't a day")] + [DataRow(15, null, "08:00", null, null, "both From and To")] + [DataRow(15, null, "22:00", "08:00", null, "later than From")] + [DataRow(15, null, "08:00", "08:00", null, "later than From")] + [DataRow(15, null, "25:00", "26:00", null, "isn't a time of day")] + [DataRow(15, null, "8am", "5pm", null, "isn't a time of day")] + [DataRow(120, null, "08:00", "09:00", null, "longer than the 08:00-09:00 window")] + [DataRow(15, null, null, null, "Mars/Olympus", "isn't a time zone")] + public void A_bad_expectation_says_what_is_wrong(int every, string day, string from, string to, string zone, string says) + { + var e = Assert.ThrowsException(() => TrafficWindow.Parse(new ExpectTraffic + { + EveryMinutes = every, Days = day == null ? null : [day], From = from, To = to, TimeZone = zone, + })); + StringAssert.Contains(e.Message, says); + } +} diff --git a/SW.Bitween.UnitTests/XchangeFileTests.cs b/SW.Bitween.UnitTests/XchangeFileTests.cs new file mode 100644 index 00000000..e80bef33 --- /dev/null +++ b/SW.Bitween.UnitTests/XchangeFileTests.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using SW.PrimitiveTypes; + +namespace SW.Bitween.UnitTests +{ + [TestClass] + public class XchangeFileTests + { + /// An exchange file travels as JSON on the bus and in storage, and comes back whole. + [TestMethod] + public void An_exchange_file_survives_a_json_round_trip() + { + var file = new XchangeFile("test data", "file.txt"); + + var back = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(file)); + + Assert.AreEqual("test data", back.Data); + Assert.AreEqual("file.txt", back.Filename); + } + } +} diff --git a/SW.Bitween.UnitTests/XchangeHistogramTests.cs b/SW.Bitween.UnitTests/XchangeHistogramTests.cs new file mode 100644 index 00000000..59c357c4 --- /dev/null +++ b/SW.Bitween.UnitTests/XchangeHistogramTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Resources.Xchanges; + +namespace SW.Bitween.UnitTests; + +/// +/// The exchange summary's histogram without a database: which bucket size a range gets, and how +/// grouped counts fold into contiguous, zero-filled buckets. +/// +[TestClass] +public class XchangeHistogramTests +{ + private static DateTime Utc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) => + new(year, month, day, hour, minute, second, DateTimeKind.Utc); + + [TestMethod] + public void A_range_gets_the_smallest_bucket_that_keeps_it_to_sixty() + { + var start = Utc(2026, 10, 11, 9); + + // Both ends count, so 59 minutes is 60 one-minute buckets and an hour is one too many. + Assert.AreEqual(1, XchangeHistogram.ChooseBucketMinutes(start, start.AddMinutes(59))); + Assert.AreEqual(5, XchangeHistogram.ChooseBucketMinutes(start, start.AddHours(1))); + Assert.AreEqual(30, XchangeHistogram.ChooseBucketMinutes(start, start.AddDays(1))); + Assert.AreEqual(180, XchangeHistogram.ChooseBucketMinutes(start, start.AddDays(7))); + Assert.AreEqual(1440, XchangeHistogram.ChooseBucketMinutes(start, start.AddDays(30))); + Assert.AreEqual(10080, XchangeHistogram.ChooseBucketMinutes(start, start.AddDays(180))); + + // Past sixty weeks there is no larger size to move to, so weeks it stays. + Assert.AreEqual(10080, XchangeHistogram.ChooseBucketMinutes(start, start.AddYears(5))); + } + + [TestMethod] + public void Every_chosen_size_gives_at_most_sixty_buckets_within_reach() + { + var start = Utc(2026, 1, 1, 0, 7, 13); + foreach (var minutes in new[] { 0, 1, 7, 59, 60, 61, 119, 300, 1439, 1441, 4000, 10079, 40000, 200000 }) + { + var end = start.AddMinutes(minutes); + var size = XchangeHistogram.ChooseBucketMinutes(start, end); + Assert.IsTrue(XchangeHistogram.BucketCount(start, end, size) <= XchangeHistogram.MaxBuckets, + $"{minutes} minutes in {size}-minute buckets"); + + // And it is the smallest that does. + var smaller = XchangeHistogram.BucketSizes.TakeWhile(s => s < size); + Assert.IsTrue(smaller.All(s => XchangeHistogram.BucketCount(start, end, s) > XchangeHistogram.MaxBuckets)); + } + } + + [TestMethod] + public void Buckets_line_up_with_the_clock_not_the_range() + { + var time = Utc(2026, 10, 15, 13, 47, 31); + + Assert.AreEqual(Utc(2026, 10, 15, 13, 47), XchangeHistogram.Floor(time, 1)); + Assert.AreEqual(Utc(2026, 10, 15, 13, 45), XchangeHistogram.Floor(time, 15)); + Assert.AreEqual(Utc(2026, 10, 15, 12), XchangeHistogram.Floor(time, 180)); + Assert.AreEqual(Utc(2026, 10, 15, 12), XchangeHistogram.Floor(time, 720)); + Assert.AreEqual(Utc(2026, 10, 15), XchangeHistogram.Floor(time, 1440)); + // 15 October 2026 is a Thursday; weeks start on Monday the 12th. + Assert.AreEqual(Utc(2026, 10, 12), XchangeHistogram.Floor(time, 10080)); + Assert.AreEqual(DateTimeKind.Utc, XchangeHistogram.Floor(time, 60).Kind); + } + + [TestMethod] + public void Folding_fills_every_bucket_and_sorts_each_count_by_status() + { + var start = Utc(2026, 10, 11, 10, 7); + var end = Utc(2026, 10, 11, 12); + + var buckets = XchangeHistogram.Fold(start, end, 5, + [ + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 10, Minute = 8, Status = false, Count = 2 }, + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 10, Minute = 9, Status = true, ResponseBad = false, Count = 3 }, + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 10, Minute = 9, Status = true, Count = 1 }, + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 11, Minute = 31, Status = null, Count = 4 }, + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 12, Minute = 0, Status = true, ResponseBad = true, Count = 5 }, + // Outside the range on both sides: dropped, not pulled onto the nearest bucket. + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 9, Minute = 59, Status = false, Count = 100 }, + new XchangeStartedCount { Year = 2026, Month = 10, Day = 11, Hour = 12, Minute = 5, Status = false, Count = 100 }, + ]); + + Assert.AreEqual(24, buckets.Count); + Assert.AreEqual(Utc(2026, 10, 11, 10, 5), buckets[0].Start); + Assert.AreEqual(Utc(2026, 10, 11, 12), buckets[^1].Start); + for (var i = 1; i < buckets.Count; i++) + Assert.AreEqual(TimeSpan.FromMinutes(5), buckets[i].Start - buckets[i - 1].Start); + + Assert.AreEqual(2, buckets[0].Failed); + Assert.AreEqual(4, buckets[0].Success); + Assert.AreEqual(4, buckets.Single(b => b.Start == Utc(2026, 10, 11, 11, 30)).Processing); + Assert.AreEqual(5, buckets[^1].BadResponse); + Assert.AreEqual(15, buckets.Sum(b => b.Processing + b.Success + b.BadResponse + b.Failed)); + + // The empty ones are there, at zero. + Assert.AreEqual(21, buckets.Count(b => b.Processing + b.Success + b.BadResponse + b.Failed == 0)); + } + + [TestMethod] + public void Day_level_counts_fold_into_weeks() + { + var start = Utc(2026, 1, 1); + var end = Utc(2027, 3, 1); + var size = XchangeHistogram.ChooseBucketMinutes(start, end); + Assert.AreEqual(10080, size); + + var buckets = XchangeHistogram.Fold(start, end, size, + [ + new XchangeStartedCount { Year = 2026, Month = 1, Day = 1, Status = false, Count = 1 }, + new XchangeStartedCount { Year = 2026, Month = 1, Day = 4, Status = false, Count = 1 }, + new XchangeStartedCount { Year = 2026, Month = 1, Day = 5, Status = false, Count = 1 }, + ]); + + // 1 January 2026 is a Thursday: its week began Monday 29 December, and runs to Sunday the 4th. + Assert.AreEqual(Utc(2025, 12, 29), buckets[0].Start); + Assert.AreEqual(2, buckets[0].Failed); + Assert.AreEqual(1, buckets[1].Failed); + } + + [TestMethod] + public void A_range_that_ends_before_it_starts_has_no_buckets() + { + var start = Utc(2026, 10, 11, 12); + Assert.AreEqual(0, XchangeHistogram.BucketCount(start, start.AddMinutes(-1), 5)); + Assert.AreEqual(0, XchangeHistogram.Fold(start, start.AddMinutes(-1), 5, []).Count); + + // A single instant is one bucket. + Assert.AreEqual(1, XchangeHistogram.Fold(start, start, 1, []).Count); + } +} diff --git a/SW.Bitween.Web/BitweenLogging.cs b/SW.Bitween.Web/BitweenLogging.cs index 28d8911d..3be55874 100644 --- a/SW.Bitween.Web/BitweenLogging.cs +++ b/SW.Bitween.Web/BitweenLogging.cs @@ -241,8 +241,8 @@ private static bool MatchesPattern(string pattern, string value) } /// - /// Rewrites one template with the retention setting added, leaving the rest of it — the ECS - /// mappings the sink depends on — exactly as the sink wrote it. + /// Rewrites one template with the retention setting added, leaving the rest of it - the ECS + /// mappings the sink depends on - exactly as the sink wrote it. /// private static void PointTemplateAtPolicy(IElasticClient client, string templateName, string policyName) { diff --git a/SW.Bitween.Web/ClientApp/README.md b/SW.Bitween.Web/ClientApp/README.md index db05033e..6a50e08e 100644 --- a/SW.Bitween.Web/ClientApp/README.md +++ b/SW.Bitween.Web/ClientApp/README.md @@ -33,7 +33,7 @@ React 19, TypeScript, Vite, Tailwind CSS 4, React Router, TanStack Query, CodeMi | `src/components/config/` | Shared configuration editors: adapters, schedules, match expressions, pickers. | | `src/components/nativeMapper/`, `src/lib/nativeMapper/` | The rules-based mapping editor. | | `src/components/mapper/`, `src/lib/mapping/` | The legacy Scriban mapping editor. | -| `e2e/` | Playwright specs and global setup. | +| `e2e/` | Playwright specs, and the `seed` setup project that prepares the database for them. `tools/e2e.sh` runs them against a throwaway instance. | ## Conventions diff --git a/SW.Bitween.Web/ClientApp/e2e/adapter-editor.spec.ts b/SW.Bitween.Web/ClientApp/e2e/adapter-editor.spec.ts new file mode 100644 index 00000000..d75f9796 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/adapter-editor.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from "@playwright/test"; +import { signInAsAdmin } from "./helpers"; + +/** + * Writing a Python adapter in the browser against a real Bitween: started from the template, + * edited, checked against the Bitween contract and tried by the server's own Python, published, + * made current, and then listed with its source like any published adapter. + */ + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("a Python handler written in the editor is checked, tried, published and made current", async ({ page }) => { + test.setTimeout(120_000); + const name = `E2ePy${Date.now()}`; + const adapterId = name.replace(/(?<=[a-z0-9])(?=[A-Z])/g, ".").toLowerCase(); + + await page.goto("adapters"); + await page.getByRole("button", { name: "New adapter" }).click(); + const dialog = page.getByRole("dialog", { name: "New adapter" }); + await dialog.getByLabel("Name").fill(name); + await dialog.getByLabel("Language").selectOption("python"); + await dialog.getByLabel("Kind").selectOption("handler"); + await dialog.getByRole("button", { name: "Start writing" }).click(); + + await expect(page).toHaveURL(/\/adapters\/drafts\/\d+$/); + await expect(page.getByRole("heading", { name: adapterId })).toBeVisible(); + const editor = page.getByTestId("draft-editor"); + await expect(editor).toHaveAttribute("data-path", "main.py"); + + // An edit, typed into the editor (Monaco): the draft is unsaved until it is checked. + await editor.locator(".monaco-editor .view-lines").click(); + // To the end of the file, as each platform's VS Code does it: Cmd+End means nothing on a Mac. + await page.keyboard.press(process.platform === "darwin" ? "Meta+ArrowDown" : "Control+End"); + await page.keyboard.type("\n# Edited in the browser.\n"); + await expect(page.getByText("Unsaved", { exact: true })).toBeVisible(); + + await page.getByRole("button", { name: "Save and check" }).click(); + await expect(page.getByText("Builds and conforms.")).toBeVisible({ timeout: 60_000 }); + await expect(page.getByText("Unsaved", { exact: true })).toHaveCount(0); + await page.getByLabel("ApiKey *").fill("e2e-key"); + + await page.getByRole("tab", { name: "Try" }).click(); + await page.getByRole("button", { name: "Save and run" }).click(); + await expect(page.getByLabel("Try result")).toContainText('"Data"', { timeout: 60_000 }); + + await page.getByRole("tab", { name: "Publish" }).click(); + await page.getByLabel("Release notes").fill("Written in the browser"); + await page.getByRole("button", { name: "Publish" }).click(); + const published = page.getByLabel("Published"); + await expect(published).toContainText("Published v1.0.0.", { timeout: 60_000 }); + await published.getByRole("button", { name: "Make v1.0.0 current" }).click(); + await expect(published).toContainText("v1.0.0 is now current"); + + // Listed with the published adapters, current at 1.0.0, its source readable. + await page.goto("adapters?tab=installed"); + const row = page.getByRole("region", { name: /^Published/ }).getByRole("button", { name: new RegExp(adapterId.replace(/\./g, "\\.")) }); + await expect(row).toContainText("v1.0.0"); + await row.click(); + await page.getByRole("button", { name: "View source of v1.0.0" }).click(); + const files = page.getByRole("list", { name: "Source files" }); + await files.getByRole("button", { name: "main.py" }).click(); + await expect(page.getByTestId("source-editor")).toContainText("Edited in the browser"); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/adapter-source.spec.ts b/SW.Bitween.Web/ClientApp/e2e/adapter-source.spec.ts new file mode 100644 index 00000000..78cdafc4 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/adapter-source.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from "@playwright/test"; +import { AdminApi } from "./api"; +import { signInAsAdmin } from "./helpers"; + +/** + * An adapter version's source, read and compared on the Adapters page against a real package in + * storage - and the read recorded in the audit trail. + */ + +/** + * Published by tools/e2e.sh: a Python handler with versions 1.0.0 and 2.0.0 (current), each + * carrying its source, and - like every adapter not on .NET - in the catalog only. + */ +const SOURCE_HANDLER = "e2e.sourcehandler"; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("an adapter only in the catalog is listed, its source is shown, and two versions are compared", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + // Not skipped when missing: it is never written to adapters/, so missing means the listing + // has stopped finding adapters that are only in the catalog. + expect(await api.hasAdapter("handlers", SOURCE_HANDLER)).toBe(true); + + await page.goto("adapters?tab=installed"); + const custom = page.getByRole("region", { name: /^Published/ }); + const row = custom.getByRole("button", { name: /Source handler \(e2e\)/ }); + await expect(row).toContainText("v2.0.0"); + await row.click(); + + await page.getByRole("button", { name: "View source of v2.0.0" }).click(); + const source = page.getByRole("region", { name: "Source of Source handler (e2e)" }); + const files = source.getByRole("list", { name: "Source files" }); + await expect(files.getByRole("button")).toHaveText(["adapter/main.py", "adapter/retry.py", "requirements.txt"]); + await expect(source.getByText("serverless build")).toBeVisible(); + + await files.getByRole("button", { name: "adapter/main.py" }).click(); + const editor = source.getByTestId("source-editor"); + await expect(editor).toHaveAttribute("data-path", "adapter/main.py"); + await expect(editor).toContainText("retries = 3"); + + await source.getByRole("combobox", { name: "Compare with" }).selectOption("1.0.0"); + await expect(files.getByRole("button")).toHaveText([ + "adapter/legacy.pyremoved", + "adapter/main.pychanged", + "adapter/retry.pyadded", + ]); + await files.getByRole("button", { name: /adapter\/main\.py/ }).click(); + await expect(editor).toHaveAttribute("data-diff", "true"); + // The diff shows the line added since 1.0.0. + await expect(editor.locator(".cm-changedLine")).toContainText("retries = 3"); + + // Every file read is in the trail, under who read it. + const trail = await api.get<{ result: { changes: Record }[] }>( + "/audit?entityName=AdapterSourceAccess&limit=200", + ); + expect( + trail.result.some( + (r) => r.changes?.AdapterId?.new === SOURCE_HANDLER && r.changes?.Path?.new === "adapter/main.py", + ), + ).toBe(true); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/api.ts b/SW.Bitween.Web/ClientApp/e2e/api.ts new file mode 100644 index 00000000..71a979cc --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/api.ts @@ -0,0 +1,142 @@ +import { expect, type APIRequestContext, type APIResponse } from "@playwright/test"; +import { ADMIN_EMAIL, ADMIN_PASSWORD, API, BASE_URL } from "./env"; + +/** + * The administrator's view of the API, for the setup a spec needs but is not about, and for + * checking afterwards that what the UI did really happened on the server. + * + * Signs in through the same endpoint the login page uses, so nothing here reaches past what a + * real administrator could do. Everything a spec builds this way is named "Playwright …" so the + * seed project's purge clears it if the spec dies before cleaning up. + */ +export class AdminApi { + private readonly request: APIRequestContext; + private readonly token: string; + + private constructor(request: APIRequestContext, token: string) { + this.request = request; + this.token = token; + } + + static async signIn(request: APIRequestContext, email = ADMIN_EMAIL, password = ADMIN_PASSWORD) { + const res = await request.post(`${API}/accounts/login`, { data: { Username: email, Password: password } }); + expect(res.ok(), `signing in to the API as ${email}: ${res.status()} ${await res.text()}`).toBe(true); + return new AdminApi(request, (await res.json()).jwt as string); + } + + private headers() { + return { Authorization: `Bearer ${this.token}` }; + } + + async raw(method: "GET" | "POST" | "DELETE", path: string, data?: unknown): Promise { + return this.request.fetch(`${API}${path}`, { + method, + headers: this.headers(), + ...(data === undefined ? {} : { data }), + }); + } + + async call(method: "GET" | "POST" | "DELETE", path: string, data?: unknown): Promise { + const res = await this.raw(method, path, data); + if (!res.ok()) throw new Error(`${method} ${path} failed: ${res.status()} ${await res.text()}`); + const text = await res.text(); + return (text ? JSON.parse(text) : null) as T; + } + + get = (path: string) => this.call("GET", path); + post = (path: string, data: unknown = {}) => this.call("POST", path, data); + delete = (path: string) => this.call("DELETE", path); + + /** The id of the row with exactly this name. */ + async idOf(list: string, name: string): Promise { + const filter = encodeURIComponent(`Name:4:${name}`); + const res = await this.get<{ result: { id: number; name: string }[] }>( + `/${list}?filter=${filter}&page=0&size=100`, + ); + const row = res.result.find((r) => r.name === name); + if (!row) throw new Error(`no ${list} row named "${name}" - did the seed project run?`); + return row.id; + } + + async account(email: string) { + const res = await this.get<{ result: { id: number; email: string; disabled?: boolean; roles: { id: number; name: string }[] | null }[] }>( + `/accounts?limit=500`, + ); + const row = res.result.find((a) => a.email.toLowerCase() === email.toLowerCase()); + if (!row) throw new Error(`no account ${email}`); + return row; + } + + /** Sends a document to one subscription, as Exchanges → New does. Find the exchange with exchangesOf. */ + async sendExchange(subscriptionId: number, document: unknown): Promise { + await this.post("/xchanges", { + option: "SubscriberId", + subscriberId: subscriptionId, + data: JSON.stringify(document), + }); + } + + /** A subscription's exchanges, newest first: status is null while running, then true or false. */ + async exchangesOf(subscriptionId: number) { + const filter = encodeURIComponent(`SubscriptionId:1:${subscriptionId}`); + const res = await this.get<{ result: { id: string; status: boolean | null; exception: string | null }[] }>( + `/xchanges?filter=${filter}&sort=StartedOn:2&page=0&size=50`, + ); + return res.result; + } + + /** Whether an adapter is published to this instance - some specs need one tools/e2e.sh publishes. */ + async hasAdapter(kind: "handlers" | "receivers" | "mappers" | "validators", id: string) { + const catalog = await this.get<{ key: string }[]>(`/adapters/Catalog?prefix=${kind}`); + return catalog.some((a) => a.key.toLowerCase() === id.toLowerCase()); + } + + createPartner(name: string) { + return this.post("/partners", { name, adapterProperties: {}, secretProperties: [], loginIdentity: null }); + } + + /** A gateway subscription that delivers nothing, so calling the gateway creates no failures. */ + createGatewaySubscription(name: string, documentId: number) { + return this.post("/subscriptions", { + name, + documentId, + type: "GatewayApiCall", + handlerId: null, + handlerProperties: [], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + inactive: false, + }); + } + + /** An API gateway with one partner attached to a subscription that delivers nothing. */ + async gatewayFor(partnerId: number, documentId: number, stamp: string) { + const urlName = `playwright-${stamp}`; + const gatewayId = await this.post("/apigateways", { + name: `Playwright Gateway ${stamp}`, + urlName, + inactive: false, + }); + const subscriptionId = await this.createGatewaySubscription(`Playwright Gateway Sub ${stamp}`, documentId); + await this.post(`/apigateways/${gatewayId}/addpartner`, { partnerId, subscriptionId }); + return { gatewayId, urlName, subscriptionId }; + } +} + +/** POSTs a document to an API gateway the way a partner would, and answers the status. */ +export async function callGateway( + request: APIRequestContext, + urlName: string, + headers: Record, +): Promise { + const res = await request.post(`${BASE_URL}api/gateway/${urlName}/async`, { + headers: { "Content-Type": "application/json", ...headers }, + data: JSON.stringify({ shipment: `e2e-gateway-${Date.now()}` }), + }); + return res.status(); +} + +/** A unique, sortable suffix for names and url names. */ +export const stamp = () => `${Date.now()}${Math.floor(Math.random() * 1000)}`; diff --git a/SW.Bitween.Web/ClientApp/e2e/audit-trail.spec.ts b/SW.Bitween.Web/ClientApp/e2e/audit-trail.spec.ts index a20302c0..f05d6291 100644 --- a/SW.Bitween.Web/ClientApp/e2e/audit-trail.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/audit-trail.spec.ts @@ -1,5 +1,6 @@ import { test, expect, type Page } from "@playwright/test"; import { + API, FIRST_PASSWORD, addMember, createRole, @@ -8,23 +9,22 @@ import { signIn, signInAsAdmin, signOut, + sessionToken, } from "./helpers"; /** * The audit trail, end to end. * * Two things make these tests different from the rest of the suite. Audit rows are never - * deleted — that is the point of the table — so nothing here may assume the trail is empty or + * deleted - that is the point of the table - so nothing here may assume the trail is empty or * that its own row is first; every assertion is scoped to the entity it created. And the - * trail's whole value rests on one negative claim — that credentials never reach it — which is + * trail's whole value rests on one negative claim - that credentials never reach it - which is * asserted against the stored row, not against what the screen happens to render. */ -const API = "https://localhost:7155/api"; - /** Reads the trail through the API with the signed-in session's own token. */ async function audit(page: Page, query: string) { - const token = await page.evaluate(() => localStorage.getItem("access_token")); + const token = await sessionToken(page); const res = await page.request.get(`${API}/audit?${query}`, { headers: { Authorization: `Bearer ${token}` }, }); @@ -55,7 +55,7 @@ test.describe("audit trail", () => { const renamed = `${name} Renamed`; await page.goto("partners"); - await page.getByRole("button", { name: "New partner" }).click(); + await page.getByRole("button", { name: "New partner" }).first().click(); const dialog = page.getByRole("dialog", { name: "New partner" }); await dialog.getByRole("textbox", { name: "Name" }).fill(name); await dialog.getByRole("button", { name: "Create partner" }).click(); @@ -63,12 +63,12 @@ test.describe("audit trail", () => { await expect(page).toHaveURL(/\/partners\/\d+$/); const id = page.url().split("/").pop()!; - // — the create shows on the partner's own page — + // - the create shows on the partner's own page - const panel = historyPanel(page); await expect(panel).toBeVisible(); await expect(panel.getByRole("row").filter({ hasText: "Added" })).toBeVisible(); - // — renaming records both sides of the change — + // - renaming records both sides of the change - await page.getByRole("textbox", { name: "Name", exact: true }).fill(renamed); await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); @@ -77,7 +77,7 @@ test.describe("audit trail", () => { // so it has to catch up on its own. It used to need a manual refresh. await expect(panel.getByRole("row").filter({ hasText: "Modified" })).toBeVisible(); - // The before/after values are reachable without a mouse — they used to be in a title, + // The before/after values are reachable without a mouse - they used to be in a title, // which keyboard and touch users can't get at. await panel.getByRole("button", { name: /Show what changed/ }).first().click(); await expect(page.getByText(`"${name}"`, { exact: false }).first()).toBeVisible(); @@ -88,7 +88,7 @@ test.describe("audit trail", () => { const modified = afterRename.result.find((r) => r.state === "Modified"); expect(modified?.changes.Name).toEqual({ old: name, new: renamed }); - // — the deletion is recorded, which the trails this replaced never did — + // - the deletion is recorded, which the trails this replaced never did - await page.getByRole("button", { name: "Delete partner" }).first().click(); await page.getByRole("button", { name: "Delete partner" }).last().click(); await expect(page).toHaveURL(/\/partners$/); @@ -102,7 +102,7 @@ test.describe("audit trail", () => { }); test("without audit.view there is no nav item, no page, and no card", async ({ page }) => { - // Everything the role needs to reach the pages the card sits on — but not the trail. + // Everything the role needs to reach the pages the card sits on - but not the trail. const role = await createRole(page, { name: `PW No Audit ${Date.now()}`, permissions: [ @@ -116,16 +116,16 @@ test.describe("audit trail", () => { await expect(page.getByRole("navigation").getByRole("link", { name: "Audit trail" })).toHaveCount(0); - // Typing the URL must not work either — hiding a link is not a permission system. + // Typing the URL must not work either - hiding a link is not a permission system. await page.goto("audit"); await expect(page.getByRole("heading", { name: "Audit trail" })).toHaveCount(0); // And the API behind it refuses, which is the check that actually matters. - const token = await page.evaluate(() => localStorage.getItem("access_token")); + const token = await sessionToken(page); const res = await page.request.get(`${API}/audit?limit=1`, { headers: { Authorization: `Bearer ${token}` }, }); - expect(res.status()).toBe(401); + expect(res.status()).toBe(403); // The card is hidden on a page this role can otherwise see in full. await page.goto("settings"); diff --git a/SW.Bitween.Web/ClientApp/e2e/browsing.spec.ts b/SW.Bitween.Web/ClientApp/e2e/browsing.spec.ts new file mode 100644 index 00000000..baa4bde9 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/browsing.spec.ts @@ -0,0 +1,186 @@ +import { test, expect } from "@playwright/test"; +import { AdminApi, stamp } from "./api"; +import { pickOption, signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * The read-only views: the Adapters page, the flow map, and the filters on the subscription + * list. Nothing here changes anything through the page; what each view shows is checked against + * rows the test made, so a view that shows the wrong thing - not just nothing - fails. + */ + +/** Published by tools/e2e.sh: a custom handler with versions 1.0.0 and 2.0.0 (current). */ +const CUSTOM_HANDLER = "e2e.samplehandler"; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("the Adapters page opens on Built-in, lists published adapters under Installed with their versions, and narrows by kind and search in each", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const hasCustom = await api.hasAdapter("handlers", CUSTOM_HANDLER); + + await page.goto("adapters"); + await expect(page.getByRole("tab", { name: /^Built-in/ })).toHaveAttribute("aria-selected", "true"); + const builtIn = page.getByRole("tabpanel"); + const custom = page.getByRole("region", { name: /^Published/ }); + + // The two HTTP adapters every subscription in this suite is built from, by their display names. + await expect(builtIn.getByRole("button", { name: /HTTP request/ })).toBeVisible(); + await expect(builtIn.getByRole("button", { name: /HTTP poll/ })).toBeVisible(); + // Built-in adapters have no versions of their own, so nothing about versions is said of them. + await expect(builtIn.getByText(/^\d+ versions?$/)).toHaveCount(0); + + // Narrowed by kind: receivers only, so the HTTP request handler goes and the HTTP poll stays. + const kind = page.getByRole("radiogroup", { name: "Kind" }); + await kind.getByRole("radio", { name: "Receivers" }).click(); + await expect(builtIn.getByRole("button", { name: /HTTP poll/ })).toBeVisible(); + await expect(builtIn.getByRole("button", { name: /HTTP request/ })).toHaveCount(0); + + // And by search. + await kind.getByRole("radio", { name: "All" }).click(); + await page.getByRole("searchbox", { name: "Search adapters" }).fill("smtp"); + await expect(builtIn.getByRole("button", { name: /Email \(SMTP\)/ })).toBeVisible(); + await expect(builtIn.getByRole("button", { name: /HTTP/ })).toHaveCount(0); + + // Published adapters are under Installed, with their own search. + await page.getByRole("tab", { name: /^Installed/ }).click(); + await expect(page).toHaveURL(/[?&]tab=installed/); + if (hasCustom) { + const row = custom.getByRole("button", { name: /Echo handler \(e2e\)/ }); + await expect(row).toContainText("v2.0.0"); + await expect(row).toContainText("2 versions"); + await expect(row).toContainText(CUSTOM_HANDLER); + await row.click(); + await expect(custom.getByRole("row").filter({ hasText: "v2.0.0" })).toContainText("Current"); + await expect(custom.getByRole("row").filter({ hasText: "v1.0.0" })).toContainText("Release 1.0.0."); + await expect(custom.getByRole("row").filter({ hasText: "ContentType" })).toContainText("text/plain"); + // No built-in adapter is listed among the installed ones. + await expect(custom.getByText("Email (SMTP)")).toHaveCount(0); + } + await page.getByRole("searchbox", { name: "Search adapters" }).fill("smtp"); + await expect(custom.getByText("No adapter matches.")).toBeVisible(); +}); + +test("the Adapters page's Marketplace tab shows its placeholder, and the tab is kept in the URL across a reload", async ({ + page, +}) => { + await page.goto("adapters"); + await page.getByRole("tab", { name: "Marketplace" }).click(); + await expect(page).toHaveURL(/[?&]tab=marketplace/); + await expect(page.getByRole("tab", { name: "Marketplace" })).toHaveAttribute("aria-selected", "true"); + await expect(page.getByText("The marketplace is on its way")).toBeVisible(); + await expect(page.getByRole("searchbox", { name: "Search adapters" })).toHaveCount(0); + + await page.reload(); + await expect(page.getByText("The marketplace is on its way")).toBeVisible(); + + await page.getByRole("tab", { name: /^Built-in/ }).click(); + await expect(page).not.toHaveURL(/tab=/); + await expect(page.getByRole("tabpanel").getByRole("button", { name: /HTTP request/ })).toBeVisible(); +}); + +test("the flow map draws an API gateway as a card that opens the gateway's page", async ({ page, request }) => { + const api = await AdminApi.signIn(request); + const s = stamp(); + const partnerId = await api.createPartner(`Playwright Map Partner ${s}`); + const g = await api.gatewayFor(partnerId, await api.idOf("documents", SEED.informationType), s); + const gatewayName = `Playwright Gateway ${s}`; + + await page.goto("flow"); + await expect(page.getByRole("heading", { name: "Flow map" })).toBeVisible(); + await expect(page.getByText(/\d+ gateways? · \d+ messages?/)).toBeVisible(); + const card = page.getByRole("link", { name: new RegExp(gatewayName) }); + await expect(card).toBeVisible(); + await card.click(); + await expect(page).toHaveURL(new RegExp(`/api-gateways/${g.gatewayId}$`)); + await expect(page.getByRole("textbox", { name: "Name", exact: true })).toHaveValue(gatewayName); + + await api.delete(`/apigateways/${g.gatewayId}`); + await api.delete(`/subscriptions/${g.subscriptionId}`); + await api.delete(`/partners/${partnerId}`); +}); + +test("the subscription list's search, type, partner and status filters each narrow it to the matching rows, and clearing the partner filter brings them back", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const s = stamp(); + const documentId = await api.idOf("documents", SEED.informationType); + const otherPartner = `Playwright Filter Partner ${s}`; + const otherPartnerId = await api.createPartner(otherPartner); + const base = { + documentId, + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: "https://example.com/filtered" }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + }; + // Two words in every name: a multi-word search once matched nothing at all. + const acme = `Playwright Filter Acme ${s}`; + const other = `Playwright Filter Other ${s}`; + const job = `Playwright Filter Job ${s}`; + const ids = [ + await api.post("/subscriptions", { + ...base, + name: acme, + type: "ApiCall", + partnerId: await api.idOf("partners", SEED.partner), + inactive: false, + }), + await api.post("/subscriptions", { ...base, name: other, type: "ApiCall", partnerId: otherPartnerId, inactive: true }), + await api.post("/subscriptions", { + ...base, + name: job, + type: "Receiving", + receiverId: "NativeHttpReceiver", + receiverProperties: [{ key: "Url", value: "https://example.com/source" }], + schedules: [{ recurrence: "Daily", days: 0, hours: 3, minutes: 0, backwards: false }], + inactive: true, + }), + ]; + + const rows = page.getByRole("row").filter({ hasText: `Playwright Filter ` }).filter({ hasText: s }); + const shown = async (...names: string[]) => { + await expect(rows).toHaveCount(names.length); + for (const n of names) await expect(rows.filter({ hasText: n })).toHaveCount(1); + }; + + await page.goto("subscriptions"); + await page.getByRole("searchbox", { name: "Search subscriptions" }).fill(`Filter Acme ${s}`); + await shown(acme); + + await page.getByRole("searchbox", { name: "Search subscriptions" }).fill(s); + await shown(acme, other, job); + + await page.getByRole("button", { name: "Scheduled job", exact: true }).click(); + await expect(page).toHaveURL(/type=Receiving/); + await shown(job); + await page.getByRole("button", { name: "Scheduled job", exact: true }).click(); + + await pickOption(page, "Filter by partner", otherPartner); + await expect(page).toHaveURL(new RegExp(`partnerId=${otherPartnerId}`)); + await shown(other); + + // Focus moves off the picker first: it opens on taking focus, and it still has it. + await page.getByRole("heading", { name: "Subscriptions", level: 1 }).click(); + await pickOption(page, "Filter by partner", "Any partner"); + await shown(acme, other, job); + await page.getByRole("combobox", { name: "Filter by status" }).selectOption("true"); + await shown(other, job); + await page.getByRole("combobox", { name: "Filter by status" }).selectOption("false"); + await shown(acme); + + // The filters live in the URL, so a reload - or a link sent to someone - shows the same rows. + await page.reload(); + await shown(acme); + + for (const id of ids) await api.delete(`/subscriptions/${id}`); + await api.delete(`/partners/${otherPartnerId}`); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts index 55696b85..f192974e 100644 --- a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts @@ -1,24 +1,20 @@ import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { signInAsAdmin } from "./helpers"; test("dashboard loads with real aggregated data", async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); await page.goto("dashboard"); await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 15000 }); - await expect(page.getByText("Exchanges today")).toBeVisible(); - await expect(page.getByText("Success rate (7 days)")).toBeVisible(); + await expect(page.getByText("Exchanges, last 24 hours")).toBeVisible(); + await expect(page.getByText("Success rate, 7 days")).toBeVisible(); + await expect(page.getByText("Traffic, last 48 hours")).toBeVisible(); + await expect(page.getByText("Housekeeping")).toBeVisible(); await expect(page.getByText("undefined")).toHaveCount(0); await expect(page.getByText("NaN")).toHaveCount(0); // "Failures to act on" counts problems rather than attempts, and has to agree with the list it - // opens — a tile whose number changes when you click it is worse than no tile. + // opens - a tile whose number changes when you click it is worse than no tile. // Anchored: the "Chains that keep failing" panel ends with an "All failures to act on" link, // which an unanchored match picks up as well. const tile = page.getByRole("link").filter({ hasText: /^Failures to act on/ }); @@ -31,7 +27,7 @@ test("dashboard loads with real aggregated data", async ({ page }) => { await expect(page.getByText(`of ${count}`)).toBeVisible({ timeout: 15000 }); // The panel that says which of those failures are not getting better, however often they are - // retried — the number on each row is how many attempts that one piece of work has taken. + // retried - the number on each row is how many attempts that one piece of work has taken. await page.goBack(); const chains = page.getByText("Chains that keep failing"); await expect(chains).toBeVisible(); diff --git a/SW.Bitween.Web/ClientApp/e2e/data-sources.spec.ts b/SW.Bitween.Web/ClientApp/e2e/data-sources.spec.ts new file mode 100644 index 00000000..2e01ecd3 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/data-sources.spec.ts @@ -0,0 +1,167 @@ +import { test, expect, type Locator, type Page } from "@playwright/test"; +import { AdminApi, stamp } from "./api"; +import { E2E_DATABASE } from "./env"; +import { signInAsAdmin } from "./helpers"; + +/** + * A PostgreSQL data source, made and used entirely through its pages, against a database that is + * certainly there: the one Bitween itself runs on. Every step goes through the real adapter - + * the connection test, the catalog the schema browser reads, and the statements it prepares. + */ + +/** Published by tools/e2e.sh; Bitween's own provider for PostgreSQL. */ +const PROVIDER = "bitween.db.postgresql"; + +test.beforeEach(async ({ page, request }) => { + const api = await AdminApi.signIn(request); + const providers = await api.get<{ adapterId: string }[]>("/datasources/providers"); + test.skip( + !providers.some((p) => p.adapterId === PROVIDER), + `${PROVIDER} is not published here - tools/e2e.sh publishes it; another instance needs it published`, + ); + await signInAsAdmin(page); +}); + +/** Creates a PostgreSQL data source through New data source, and lands on its page. */ +async function createDataSource(page: Page, name: string) { + await page.goto("data-sources"); + await page.getByRole("button", { name: "New data source" }).first().click(); + await page.fill("#ds-name", name); + await page.locator("#ds-provider").selectOption(PROVIDER); + // Asked for at create: what the provider can't connect without. The tests set the real + // connection on the data source's page afterwards. + const required = { Host: E2E_DATABASE.host, Database: E2E_DATABASE.database, UserName: E2E_DATABASE.user, Password: "placeholder" }; + for (const [setting, value] of Object.entries(required)) { + const field = page.locator(`#ds-new-${setting}`); + if (await field.count()) await field.fill(value); + } + await page.getByRole("button", { name: "Create", exact: true }).click(); + await expect(page).toHaveURL(/\/data-sources\/\d+$/); + await expect(page.getByRole("heading", { name, level: 1 })).toBeVisible(); + return Number(new URL(page.url()).pathname.split("/").pop()); +} + +async function fillConnection(page: Page, password: string) { + await page.fill("#ds-prop-Host", E2E_DATABASE.host); + await page.fill("#ds-prop-Port", E2E_DATABASE.port); + await page.fill("#ds-prop-Database", E2E_DATABASE.database); + await page.fill("#ds-prop-UserName", E2E_DATABASE.user); + await page.fill("#ds-prop-Password", password); +} + +async function save(page: Page) { + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); +} + +/** The open "Add statement" form: the innermost box holding both its SQL and its button. */ +const statementForm = (page: Page, panel: Locator) => + panel + .locator("div") + .filter({ has: page.locator("textarea") }) + .filter({ has: page.getByRole("button", { name: "Create statement" }) }) + .last(); + +async function testConnection(page: Page) { + await page.getByRole("button", { name: "Test connection" }).click(); + const verdict = page.getByRole("heading", { name: /^The connection (works|failed)$/ }); + await expect(verdict).toBeVisible({ timeout: 60_000 }); + return verdict; +} + +test("a PostgreSQL data source pointed at the e2e database: a wrong password fails the connection test, the right one passes, and the password is never shown again", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const id = await createDataSource(page, `Playwright Warehouse ${stamp()}`); + + await fillConnection(page, "not-the-password"); + await save(page); + await expect(await testConnection(page)).toHaveText("The connection failed"); + + await page.fill("#ds-prop-Password", E2E_DATABASE.password); + await save(page); + await expect(await testConnection(page)).toHaveText("The connection works"); + + // Stored, and masked from then on - on the page and from the API. + await page.reload(); + await expect(page.locator("#ds-prop-Password")).toHaveValue(""); + await expect(page.locator("#ds-prop-Password")).toHaveAttribute("placeholder", "••••••••"); + const stored = await api.get<{ properties: Record }>(`/datasources/${id}`); + expect(stored.properties.Password).not.toBe(E2E_DATABASE.password); + expect(stored.properties.Host).toBe(E2E_DATABASE.host); + + await api.delete(`/datasources/${id}`); +}); + +test("the schema browser lists the database's tables and opens one to its columns, and a statement written from it is prepared against the live schema", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const id = await createDataSource(page, `Playwright Catalog ${stamp()}`); + await fillConnection(page, E2E_DATABASE.password); + await save(page); + await expect(await testConnection(page)).toHaveText("The connection works"); + + // The test runs on a throwaway connection; the browser asks the adapter the node holds open, + // which the supervisor starts within its next pass after the save. + await expect + .poll(async () => (await api.get<{ lastKnownState: string | null }>(`/datasources/${id}`)).lastKnownState ?? "", { + message: "the node never started the data source's adapter", + timeout: 60_000, + }) + .toMatch(/connected|ready|idle|running/i); + await page.reload(); + + // Bitween's own tables are what this database holds; infolink.partner is one of them. + const browser = page.locator("section").filter({ + has: page.getByRole("heading", { name: "What is in the database", exact: true }), + }); + await expect(browser.getByText(/^PostgreSQL /)).toBeVisible({ timeout: 30_000 }); + await browser.getByRole("textbox", { name: "Search the catalog by name" }).fill("partner"); + const table = browser.getByRole("listitem").filter({ has: page.getByText("partner", { exact: true }) }).first(); + await expect(table).toBeVisible({ timeout: 30_000 }); + await table.getByRole("button", { expanded: false }).click(); + await expect(table.getByText("login_identity", { exact: true })).toBeVisible({ timeout: 30_000 }); + + // A statement drafted from the table, saved - which prepares it against the database. + await table.getByRole("button", { name: "Use in a statement" }).click(); + const statements = page.locator("section").filter({ + has: page.getByRole("heading", { name: "SQL statements", exact: true }), + }); + const form = statementForm(page, statements); + const sql = form.locator("textarea"); + await expect(sql).toHaveValue(/partner/); + const statementName = `partnersByName${stamp()}`; + await form.getByRole("textbox").first().fill(statementName); + await sql.fill(`select id, name from infolink.partner where name = @name`); + await form.getByRole("button", { name: "Create statement" }).click(); + await expect(statements.getByText(statementName)).toBeVisible(); + await expect(statements.getByText("Saved, but not checked")).toHaveCount(0); + + // SQL naming a column that isn't there is refused when it is saved, saying what is wrong. + await statements.getByRole("button", { name: "Add statement" }).click(); + const bad = statementForm(page, statements); + await bad.getByRole("textbox").first().fill(`badColumn${stamp()}`); + await bad.locator("textarea").fill(`select no_such_column from infolink.partner`); + await bad.getByRole("button", { name: "Create statement" }).click(); + await expect(bad.getByRole("alert")).toContainText(/no_such_column/i); + + // The connection test now prepares the saved statement too, and it passes. + await expect(await testConnection(page)).toHaveText("The connection works"); + await expect(page.getByText(`statement:${statementName}`)).toBeVisible(); + + const filter = encodeURIComponent(`DataSourceId:1:${id}`); + const { result: rows } = await api.get<{ result: { id: number; name: string; sql: string }[] }>( + `/datasourcestatements?filter=${filter}&offset=0&limit=100`, + ); + expect(rows.map((r) => r.name)).toEqual([statementName]); + expect(rows[0].sql).toBe(`select id, name from infolink.partner where name = @name`); + + for (const row of rows) await api.delete(`/datasourcestatements/${row.id}`); + await api.delete(`/datasources/${id}`); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/env.ts b/SW.Bitween.Web/ClientApp/e2e/env.ts new file mode 100644 index 00000000..91615fb6 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/env.ts @@ -0,0 +1,42 @@ +/** + * Where the suite points and who it signs in as, in one place. + * + * Read from the environment because the answer differs by database. A fresh install refuses to + * start until it is given an administrator password of its own (Bitween__InitialAdminPassword), + * so tools/e2e.sh chooses one and passes it in; a team dev database migrated before that rule + * still has the old published one, which stays the default so running `yarn test:e2e` against it + * keeps working without any setup. + * + * Deliberately free of Playwright imports: playwright.config.ts reads it too. + */ + +export const BASE_URL = (process.env.E2E_BASE_URL ?? "https://localhost:7155/").replace(/\/?$/, "/"); + +/** The backend's API root, for the specs and the seed that call it directly. */ +export const API = `${BASE_URL}api`; + +export const ADMIN_EMAIL = process.env.E2E_ADMIN_EMAIL ?? "admin@Bitween.systems"; +export const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? "Mtm@dmin!2"; + +/** + * The broker's management API, which the queue health specs use to put messages where only a + * failure in production would - a dead letter, a queue nothing reads. tools/e2e.sh sets the URL. + */ +export const E2E_RABBITMQ_MANAGEMENT = { + url: (process.env.E2E_MQ_MGMT_URL ?? "http://localhost:55673").replace(/\/$/, ""), + user: process.env.E2E_MQ_USER ?? "guest", + password: process.env.E2E_MQ_PASSWORD ?? "guest", +}; + +/** + * The database the instance under test runs on, as seen from this machine. The data source specs + * connect a PostgreSQL data source to it: it is the one database certain to be there. tools/e2e.sh + * sets the port; the rest is what its container is started with. + */ +export const E2E_DATABASE = { + host: process.env.E2E_PG_HOST ?? "localhost", + port: process.env.E2E_PG_PORT ?? "55432", + database: process.env.E2E_PG_DATABASE ?? "bitween", + user: process.env.E2E_PG_USER ?? "postgres", + password: process.env.E2E_PG_PASSWORD ?? "postgres", +}; diff --git a/SW.Bitween.Web/ClientApp/e2e/exchange-operations.spec.ts b/SW.Bitween.Web/ClientApp/e2e/exchange-operations.spec.ts new file mode 100644 index 00000000..f17f76f4 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/exchange-operations.spec.ts @@ -0,0 +1,256 @@ +import { test, expect, type Page } from "@playwright/test"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { inflateRawSync } from "node:zlib"; +import { AdminApi, stamp } from "./api"; +import { signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * What the Exchanges page does to exchanges - retry them in bulk and one at a time, with and + * without re-resolving the subscription's settings, and export their files - each followed to + * its effect: deliveries arriving at an endpoint the test runs, the API's record of the chain, + * and the bytes of the download. + * + * Every failure a test here creates is retried into a success before it ends. The exchanges spec + * retries whichever failures are newest, and a failure left behind by a subscription deleted here + * would be one it can't retry. + */ + +/** A local endpoint that records the bodies sent to it. */ +async function startEndpoint() { + const received: string[] = []; + const server = createServer((req, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + received.push(body); + res.end("ok"); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; + return { url, received, close: () => new Promise((resolve) => server.close(() => resolve())) }; +} + +/** A live subscription delivering over HTTP to `url`. */ +async function subscriptionTo(api: AdminApi, name: string, url: string) { + return api.post("/subscriptions", { + name, + documentId: await api.idOf("documents", SEED.informationType), + type: "ApiCall", + partnerId: await api.idOf("partners", SEED.partner), + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: url }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + retryPolicyId: null, + inactive: false, + }); +} + +/** The subscription's exchanges by outcome, counting only the newest attempt of each chain. */ +async function outcomes(api: AdminApi, subscriptionId: number) { + const filter = ["LatestOnly:1:true", `SubscriptionId:1:${subscriptionId}`] + .map((f) => `filter=${encodeURIComponent(f)}`) + .join("&"); + const { result } = await api.get<{ result: { id: string; status: boolean | null }[] }>( + `/xchanges?${filter}&page=0&size=200`, + ); + return { + failed: result.filter((x) => x.status === false).map((x) => x.id), + succeeded: result.filter((x) => x.status === true).map((x) => x.id), + running: result.filter((x) => x.status === null).length, + }; +} + +/** Points the subscription's delivery somewhere else, on its page. */ +async function changeDeliveryUrl(page: Page, subscriptionId: number, url: string) { + await page.goto(`subscriptions/${subscriptionId}?stage=delivery`); + await page.locator("#prop-Url").fill(url); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); +} + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("selecting all matching failures across pages and retrying them with re-resolved settings runs every one again against the subscription's new URL", async ({ + page, + request, +}) => { + test.setTimeout(180_000); + const api = await AdminApi.signIn(request); + const endpoint = await startEndpoint(); + const id = await subscriptionTo(api, `Playwright Bulk Retry ${stamp()}`, SEED.failingUrl); + // One more than a page holds, so "all matching" means more than what is on screen. + const count = 26; + + try { + for (let i = 0; i < count; i++) await api.sendExchange(id, { shipment: `bulk-${i}` }); + await expect + .poll(async () => (await outcomes(api, id)).failed.length, { + message: `the ${count} exchanges never all failed`, + timeout: 90_000, + }) + .toBe(count); + + // The fix: deliver somewhere that answers. Only a retry that re-resolves picks it up. + await changeDeliveryUrl(page, id, endpoint.url); + + await page.goto(`exchanges?subscriptionId=${id}&status=failed&latest=1`); + await page.getByLabel("Refresh interval").selectOption("0"); + await expect(page.getByRole("checkbox", { name: /^Select [0-9a-f]{32}$/ })).toHaveCount(25); + await page.getByRole("checkbox", { name: "Select all on this page" }).check(); + await expect(page.getByText("Only the 25 rows on this page.")).toBeVisible(); + await page.getByRole("button", { name: `Select all ${count} matching this filter` }).click(); + await expect(page.getByText("- everything this filter matches")).toBeVisible(); + + await page.getByRole("button", { name: "Retry selected…" }).click(); + const dialog = page.getByRole("dialog", { name: `Retry ${count} exchanges?` }); + await expect(dialog).toContainText(`${count} exchanges will run again`); + await dialog.getByRole("checkbox", { name: /^Re-resolve adapter properties/ }).check(); + await dialog.getByRole("button", { name: "Retry" }).click(); + await expect(page.getByText(`${count} retries started.`)).toBeVisible({ timeout: 30_000 }); + + // Every one of them, not just the page that was showing, delivered to the new URL. + await expect + .poll(() => endpoint.received.length, { message: "not every retry was delivered", timeout: 90_000 }) + .toBe(count); + await expect + .poll(async () => { + const o = await outcomes(api, id); + return { failed: o.failed.length, succeeded: o.succeeded.length }; + }, { timeout: 60_000 }) + .toEqual({ failed: 0, succeeded: count }); + } finally { + await endpoint.close(); + } + await api.delete(`/subscriptions/${id}`); +}); + +test("retrying a failure without re-resolving reuses the settings it first ran with and fails again; retrying with them re-resolved delivers to the new URL", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const endpoint = await startEndpoint(); + const id = await subscriptionTo(api, `Playwright Reset Retry ${stamp()}`, SEED.failingUrl); + + /** Opens one exchange's drawer and retries it, re-resolving or not. */ + const retry = async (exchangeId: string, reResolve: boolean) => { + await page.goto(`exchanges?ids=${exchangeId}`); + await page.getByLabel("Refresh interval").selectOption("0"); + // A link to one exchange opens it beside the list. + await expect(page.getByRole("complementary", { name: `Exchange ${exchangeId}` })).toBeVisible({ timeout: 15000 }); + await page.getByRole("button", { name: "Retry…" }).click(); + const dialog = page.getByRole("dialog", { name: "Retry this exchange?" }); + const box = dialog.getByRole("checkbox", { name: /^Re-resolve adapter properties/ }); + await expect(box).not.toBeChecked(); + if (reResolve) await box.check(); + await dialog.getByRole("button", { name: "Retry" }).click(); + await expect(page.getByText(/Retry started/)).toBeVisible({ timeout: 15000 }); + }; + + try { + await api.sendExchange(id, { shipment: "reset-me" }); + await expect.poll(async () => (await outcomes(api, id)).failed.length, { timeout: 60_000 }).toBe(1); + const [original] = (await outcomes(api, id)).failed; + + await changeDeliveryUrl(page, id, endpoint.url); + + // As it first ran: the old, unreachable URL travels with the exchange. + await retry(original, false); + await expect + .poll(async () => (await outcomes(api, id)).failed, { message: "the plain retry never finished", timeout: 60_000 }) + .not.toEqual([original]); + const [again] = (await outcomes(api, id)).failed; + expect(again).not.toBe(original); + expect(endpoint.received).toEqual([]); + + // Re-resolved: the subscription's settings as they are now. + await retry(again, true); + await expect + .poll(async () => { + const o = await outcomes(api, id); + return { failed: o.failed.length, succeeded: o.succeeded.length }; + }, { message: "the re-resolved retry never succeeded", timeout: 60_000 }) + .toEqual({ failed: 0, succeeded: 1 }); + expect(endpoint.received).toHaveLength(1); + expect(endpoint.received[0]).toContain("reset-me"); + } finally { + await endpoint.close(); + } + await api.delete(`/subscriptions/${id}`); +}); + +test("Export files on two selected exchanges downloads one zip holding a folder for each, with the document each one carried", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const endpoint = await startEndpoint(); + const s = stamp(); + const id = await subscriptionTo(api, `Playwright Export ${s}`, endpoint.url); + + try { + await api.sendExchange(id, { shipment: `export-a-${s}` }); + await api.sendExchange(id, { shipment: `export-b-${s}` }); + await expect.poll(async () => (await outcomes(api, id)).succeeded.length, { timeout: 60_000 }).toBe(2); + const ids = (await outcomes(api, id)).succeeded; + + await page.goto(`exchanges?ids=${ids.join(",")}`); + await page.getByLabel("Refresh interval").selectOption("0"); + for (const x of ids) await page.getByRole("checkbox", { name: `Select ${x}`, exact: true }).check(); + await expect(page.getByText("2 selected")).toBeVisible(); + + const download = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export files" }).click(); + const file = await download; + expect(file.suggestedFilename()).toMatch(/\.zip$/); + const zip = await readStream(await file.createReadStream()); + + // A folder per exchange, named by its id, holding the document that exchange carried. + const entries = zipEntries(zip); + for (const x of ids) { + const mine = entries.filter((e) => e.name.startsWith(`${x}/`)); + expect(mine.length, `no files for ${x} in ${entries.map((e) => e.name).join(", ")}`).toBeGreaterThan(0); + expect(mine.map((e) => e.content).join("\n")).toMatch(new RegExp(`export-[ab]-${s}`)); + } + } finally { + await endpoint.close(); + } + await api.delete(`/subscriptions/${id}`); +}); + +/** The entries of a zip, read through its central directory; stored or deflated. */ +function zipEntries(zip: Buffer): { name: string; content: string }[] { + const entries: { name: string; content: string }[] = []; + const end = zip.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06])); + let at = zip.readUInt32LE(end + 16); + for (let i = 0; i < zip.readUInt16LE(end + 10); i++) { + const method = zip.readUInt16LE(at + 10); + const size = zip.readUInt32LE(at + 20); + const nameLength = zip.readUInt16LE(at + 28); + const extraLength = zip.readUInt16LE(at + 30); + const commentLength = zip.readUInt16LE(at + 32); + const local = zip.readUInt32LE(at + 42); + const name = zip.subarray(at + 46, at + 46 + nameLength).toString("utf8"); + const dataAt = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28); + const data = zip.subarray(dataAt, dataAt + size); + entries.push({ name, content: (method === 8 ? inflateRawSync(data) : data).toString("utf8") }); + at += 46 + nameLength + extraLength + commentLength; + } + return entries; +} + +async function readStream(stream: NodeJS.ReadableStream): Promise { + const chunks: Buffer[] = []; + for await (const chunk of stream) chunks.push(Buffer.from(chunk as Buffer)); + return Buffer.concat(chunks); +} diff --git a/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts index c91aed3f..5233870e 100644 --- a/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts @@ -1,16 +1,34 @@ -import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { test, expect, type Page } from "@playwright/test"; +import { API, sessionToken, signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); +/** + * The ids of `count` failed exchanges that have not been retried, newest first. Polled because a + * retry made moments ago is still being processed, and only becomes a failure nobody has retried + * once it has run. + */ +async function unretriedFailures(page: Page, count: number): Promise { + const token = await sessionToken(page); + const query = ["StatusFilter:1:3", "LatestOnly:1:true"].map((f) => `filter=${encodeURIComponent(f)}`).join("&"); + let ids: string[] = []; + await expect + .poll( + async () => { + const res = await page.request.get(`${API}/xchanges?${query}&sort=StartedOn:2&page=0&size=${count}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + ids = ((await res.json()) as { result: { id: string }[] }).result.map((x) => x.id); + return ids.length; + }, + { message: `fewer than ${count} un-retried failed exchanges - did the seed project run?`, timeout: 15000 }, + ) + .toBeGreaterThanOrEqual(count); + return ids; +} + test("exchanges list, filter, retry, bulk retry, create", async ({ page }) => { test.setTimeout(45000); await page.goto("exchanges"); @@ -22,39 +40,44 @@ test("exchanges list, filter, retry, bulk retry, create", async ({ page }) => { await page.getByLabel("Refresh interval").selectOption("0"); // Filter down to failed exchanges only. - await page.getByRole("button", { name: "Failed" }).click(); + // By the start of its name: the button carries its count ("Failed 12"), and the volume chart's + // bars mention failures too. + await page.getByRole("button", { name: /^Failed\b/ }).click(); + await expect(page).toHaveURL(/status=failed/); // Pick by content, not position: the filter re-renders the table, and an index would race it // and land on whichever row was showing before. - const row = page.getByRole("row").filter({ hasText: "Failed" }).first(); - await expect(row).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("row").filter({ hasText: "Failed" }).first()).toBeVisible({ timeout: 10000 }); - // Expand the row (click the chevron cell — other cells stop propagation) - // and retry it from the drawer. - await row.locator("td").last().click(); + // Which exchanges to retry is asked of the API rather than read off the top of the list. The + // newest rows are whatever the last run left there: a retry still in flight, or a success + // whose subscription has since been deleted - neither can be retried, and a bulk retry of + // nothing but those offers no Retry button to press. The seed keeps at least three failures + // nobody has retried yet; these are three of them. + const [single, ...pair] = await unretriedFailures(page, 3); + + // A link to one exchange opens it beside the list; retry it from there. + await page.goto(`exchanges?ids=${single}`); + await expect(page.getByRole("complementary", { name: `Exchange ${single}` })).toBeVisible({ timeout: 15000 }); await page.getByRole("button", { name: "Retry…" }).click(); await page.getByRole("dialog").getByRole("button", { name: "Retry" }).click(); await expect(page.getByText(/Retry started/)).toBeVisible({ timeout: 10000 }); - // Bulk retry a couple of specific rows (not the whole page — each retry does + // Bulk retry a couple of specific rows (not the whole page - each retry does // real file I/O against storage, so keep this fast and deterministic). - await page.getByRole("button", { name: "All" }).click(); + await page.goto(`exchanges?ids=${pair.join(",")}`); + await page.getByLabel("Refresh interval").selectOption("0"); // Row checkboxes only. "Select all on this page" also starts with "Select", and its checked - // state is derived from every row on the page — so a refetch landing mid-click (the filter - // above triggers one) flips it back and reads as a click that did nothing. A row checkbox is - // keyed by its own id and survives that. It also keeps the bulk retry to two rows, which is - // what this test says it wants: each retry is real file I/O. - const rowCheckbox = page.getByRole("checkbox", { name: /^Select (?!all\b)/ }); - await expect(rowCheckbox.first()).toBeVisible({ timeout: 10000 }); - await rowCheckbox.nth(0).check(); - await rowCheckbox.nth(1).check(); - await expect(page.getByText(/\d+ selected/)).toBeVisible(); + // state is derived from every row on the page - so a refetch landing mid-click flips it back + // and reads as a click that did nothing. A row checkbox is keyed by its own id and survives that. + for (const id of pair) await page.getByRole("checkbox", { name: `Select ${id}`, exact: true }).check(); + await expect(page.getByText("2 selected")).toBeVisible(); await page.getByRole("button", { name: "Retry selected…" }).click(); await page.getByRole("dialog").getByRole("button", { name: "Retry" }).click(); await expect(page.getByRole("dialog")).toHaveCount(0, { timeout: 15000 }); // Manually create an exchange addressed at a subscription. await page.goto("exchanges/new"); - await page.getByRole("combobox", { name: "Pick a subscription…" }).click(); + await page.getByRole("combobox", { name: "Subscription" }).click(); await page.getByRole("option").first().click(); // Dismiss the dropdown panel via an outside click (it sits above the panel's // anchor point, so it can't itself be covered) rather than Escape, which @@ -74,9 +97,19 @@ test("scheduled retries page loads", async ({ page }) => { }); test("queue health page loads with live consumer data", async ({ page }) => { + // Queue names start with the environment's name ("v3.development.bitween…", "v3.local.bitween…"), + // so which one to look for is asked of the same API the page reads rather than assumed. + const token = await sessionToken(page); + const res = await page.request.get(`${API}/ops/consumers`, { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(res.ok()).toBeTruthy(); + const consumers = (await res.json()) as { queueName: string }[]; + expect(consumers.length, "the broker reports no consumers at all").toBeGreaterThan(0); + await page.goto("queue-health"); await expect(page.getByRole("heading", { name: "Queue health" })).toBeVisible({ timeout: 15000 }); - await expect(page.getByText("v3.local.bitween").first()).toBeVisible({ timeout: 10000 }); + await expect(page.getByText(consumers[0].queueName, { exact: true }).first()).toBeVisible({ timeout: 10000 }); await expect(page.getByText("undefined")).toHaveCount(0); }); @@ -93,7 +126,7 @@ test("a retried exchange shows its chain and sends you to the newest attempt", a // Whichever failed exchange has not been retried yet. Most have not, but this database // accumulates chains as the suite runs, and an already-retried one has no Retry button to - // press — which is the very thing under test further down. + // press - which is the very thing under test further down. let retried: string | null = null; const failedRows = page.getByRole("row").filter({ hasText: "Failed" }); // The whole page, not the first few rows: this database accumulates chains as the suite runs, @@ -124,7 +157,8 @@ test("a retried exchange shows its chain and sends you to the newest attempt", a await page.goto(`exchanges?ids=${retried}`); const row = page.locator(`tr:has(input[aria-label="Select ${retried}"])`); await expect(row).toBeVisible({ timeout: 15000 }); - await row.locator("td").last().click(); + // A link to one exchange opens it: nothing to click. + await expect(page.getByRole("complementary", { name: `Exchange ${retried}` })).toBeVisible(); await expect(page.getByText("Already retried")).toBeVisible({ timeout: 10000 }); await expect(page.getByRole("button", { name: "Retry…" })).toHaveCount(0); diff --git a/SW.Bitween.Web/ClientApp/e2e/gateway-access.spec.ts b/SW.Bitween.Web/ClientApp/e2e/gateway-access.spec.ts new file mode 100644 index 00000000..3e2ce4f9 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/gateway-access.spec.ts @@ -0,0 +1,307 @@ +import { test, expect, type Page } from "@playwright/test"; +import { createServer, type Server } from "node:http"; +import { generateKeyPairSync, randomUUID, sign, type KeyObject } from "node:crypto"; +import type { AddressInfo } from "node:net"; +import { AdminApi, callGateway, stamp } from "./api"; +import { signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * Who can call an API gateway, decided on the pages where it is configured: keys issued and + * revoked on the partner's page, and how the gateway asks callers to prove who they are on the + * gateway's own. Each change is checked the only way that matters - by calling the gateway the + * way a partner would, from outside the browser. + * + * The gateways here attach their partner to a subscription that delivers nothing, so a call that + * gets in creates an exchange (202) without leaving a failed delivery behind. + */ + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +/** A partner with a gateway it is attached to. */ +async function partnerWithGateway(api: AdminApi) { + const s = stamp(); + const partnerName = `Playwright Caller ${s}`; + const partnerId = await api.createPartner(partnerName); + const documentId = await api.idOf("documents", SEED.informationType); + const gateway = await api.gatewayFor(partnerId, documentId, s); + return { partnerId, partnerName, ...gateway }; +} + +async function cleanUp(api: AdminApi, g: { gatewayId: number; subscriptionId: number; partnerId: number }) { + await api.delete(`/apigateways/${g.gatewayId}`); + await api.delete(`/subscriptions/${g.subscriptionId}`); + await api.delete(`/partners/${g.partnerId}`); +} + +/** Issues a key on the partner's page and returns it, read off the one screen that shows it. */ +async function issueKey(page: Page, partnerId: number, keyName: string): Promise { + await page.goto(`partners/${partnerId}`); + await page.getByRole("button", { name: "New key" }).click(); + const dialog = page.getByRole("dialog", { name: "New API key" }); + await dialog.locator("#ak-name").fill(keyName); + await dialog.getByRole("button", { name: "Generate key" }).click(); + + const shown = page.getByRole("dialog", { name: "API key created" }); + await expect(shown.getByText(`Key "${keyName}"`)).toBeVisible(); + const key = (await shown.locator("code").textContent())?.trim() ?? ""; + expect(key, "the issued key").toMatch(/^[0-9a-f]{32}$/); + await shown.getByRole("button", { name: "Done" }).click(); + await expect(shown).toHaveCount(0); + return key; +} + +/** The partner page's "API keys" panel. */ +const keysTable = (page: Page) => + page.locator("section").filter({ has: page.getByRole("heading", { name: "API keys", exact: true }) }); + +test("a key issued on the partner page is shown once, opens the gateway, and is refused once revoked while the partner's other key keeps working", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + + // No key yet: the gateway turns the partner away. + expect(await callGateway(request, g.urlName, { partnerkey: "not-a-key" })).toBe(401); + + const production = await issueKey(page, g.partnerId, "Production"); + const staging = await issueKey(page, g.partnerId, "Staging"); + + // Afterwards the page lists the keys by name and prefix, and the secret itself is gone for good. + await page.reload(); + const keys = keysTable(page); + await expect(keys.getByRole("row").filter({ hasText: "Production" })).toContainText(`${production.slice(0, 4)}`); + await expect(page.getByText(production)).toHaveCount(0); + await expect(page.getByText(staging)).toHaveCount(0); + + expect(await callGateway(request, g.urlName, { partnerkey: production })).toBe(202); + expect(await callGateway(request, g.urlName, { partnerkey: staging })).toBe(202); + + await keys.getByRole("row").filter({ hasText: "Production" }).getByRole("button", { name: "Revoke" }).click(); + await page + .getByRole("dialog", { name: "Revoke this API key?" }) + .getByRole("button", { name: "Revoke key" }) + .click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(keys.getByRole("row").filter({ hasText: "Production" })).toHaveCount(0); + await expect(keys.getByRole("row").filter({ hasText: "Staging" })).toBeVisible(); + + expect(await callGateway(request, g.urlName, { partnerkey: production })).toBe(401); + // Revoking rewrites the partner's credentials, so the key left behind has to survive that. + expect(await callGateway(request, g.urlName, { partnerkey: staging })).toBe(202); + + await cleanUp(api, g); +}); + +test("a custom key header set on the gateway page: the key works in it, in partnerkey, as a Bearer token and as Basic auth, and not in any other header", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + const key = await issueKey(page, g.partnerId, "Main"); + + await page.goto(`api-gateways/${g.gatewayId}`); + await page.fill("#ag-key-header", "X-Acme-Key"); + // The call preview follows the draft and says it isn't live yet. + await expect(page.getByText("- with your unsaved changes")).toBeVisible(); + await expect(page.getByLabel("Ways to send the key")).toContainText("X-Acme-Key: "); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + await page.reload(); + await expect(page.locator("#ag-key-header")).toHaveValue("X-Acme-Key"); + + const basic = (name: string) => `Basic ${Buffer.from(`${name}:${key}`).toString("base64")}`; + expect(await callGateway(request, g.urlName, { "X-Acme-Key": key })).toBe(202); + expect(await callGateway(request, g.urlName, { partnerkey: key })).toBe(202); + expect(await callGateway(request, g.urlName, { Authorization: `Bearer ${key}` })).toBe(202); + expect(await callGateway(request, g.urlName, { Authorization: basic("Main") })).toBe(202); + // Basic carries the key's name as the username, and it has to be this key's. + expect(await callGateway(request, g.urlName, { Authorization: basic("Someone else") })).toBe(401); + expect(await callGateway(request, g.urlName, { "X-Other-Key": key })).toBe(401); + expect(await callGateway(request, g.urlName, {})).toBe(401); + + await cleanUp(api, g); +}); + +/** + * A login server of our own: the discovery document and signing keys a JWT gateway reads, served + * from this test process. Bitween accepts plain http for a loopback login server, for exactly this. + */ +async function startLoginServer() { + const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const kid = randomUUID(); + let issuer = ""; + const server: Server = createServer((req, res) => { + res.setHeader("Content-Type", "application/json"); + if (req.url === "/.well-known/openid-configuration") + res.end(JSON.stringify({ issuer, jwks_uri: `${issuer}/jwks` })); + else if (req.url === "/jwks") + res.end(JSON.stringify({ keys: [{ ...publicKey.export({ format: "jwk" }), kid, use: "sig", alg: "RS256" }] })); + else { + res.statusCode = 404; + res.end("{}"); + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + issuer = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + + const token = (claims: Record, key: KeyObject = privateKey) => { + const now = Math.floor(Date.now() / 1000); + const part = (o: unknown) => Buffer.from(JSON.stringify(o)).toString("base64url"); + const body = `${part({ alg: "RS256", typ: "JWT", kid })}.${part({ iss: issuer, iat: now, nbf: now, exp: now + 600, ...claims })}`; + return `${body}.${sign("sha256", Buffer.from(body), key).toString("base64url")}`; + }; + + return { issuer, token, close: () => new Promise((resolve) => server.close(() => resolve())) }; +} + +test("a gateway switched to login-server tokens on its page refuses API keys, accepts a token naming the partner's login identity, and refuses a token for another audience, another partner or a forged signature", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + const key = await issueKey(page, g.partnerId, "Main"); + const login = await startLoginServer(); + const identity = `acme-${stamp()}`; + + try { + // The partner's side: which identity its tokens carry. + await page.goto(`partners/${g.partnerId}`); + await page.fill("#pf-login-identity", identity); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + // The gateway's side: take tokens from that login server instead of keys. + await page.goto(`api-gateways/${g.gatewayId}`); + await page.locator("#ag-auth-method").selectOption("Jwt"); + await page.fill("#ag-jwt-issuer", login.issuer); + await page.fill("#ag-jwt-audience", "bitween-e2e"); + await page.getByRole("button", { name: "Save changes" }).click(); + const confirm = page.getByRole("dialog", { name: "Change how partners authenticate?" }); + await expect(confirm).toContainText("Partners calling with API keys will get 401s."); + await confirm.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + + // Saved as typed, and the attached partner's identity shows in the table. + await page.reload(); + await expect(page.locator("#ag-auth-method")).toHaveValue("Jwt"); + await expect(page.locator("#ag-jwt-issuer")).toHaveValue(login.issuer); + await expect(page.getByRole("row").filter({ hasText: g.partnerName })).toContainText(identity); + + const bearer = (t: string) => ({ Authorization: `Bearer ${t}` }); + expect(await callGateway(request, g.urlName, { partnerkey: key })).toBe(401); + expect(await callGateway(request, g.urlName, bearer(key))).toBe(401); + expect(await callGateway(request, g.urlName, bearer(login.token({ aud: "bitween-e2e", sub: identity })))).toBe(202); + expect(await callGateway(request, g.urlName, bearer(login.token({ aud: "someone-else", sub: identity })))).toBe(401); + expect(await callGateway(request, g.urlName, bearer(login.token({ aud: "bitween-e2e", sub: "nobody-we-know" })))).toBe(401); + const forger = generateKeyPairSync("rsa", { modulusLength: 2048 }).privateKey; + expect( + await callGateway(request, g.urlName, bearer(login.token({ aud: "bitween-e2e", sub: identity }, forger))), + ).toBe(401); + + // And back to keys: the key works again, the token no longer does. + await page.locator("#ag-auth-method").selectOption("PartnerKey"); + await page.getByRole("button", { name: "Save changes" }).click(); + await page + .getByRole("dialog", { name: "Change how partners authenticate?" }) + .getByRole("button", { name: "Save changes" }) + .click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + expect(await callGateway(request, g.urlName, { partnerkey: key })).toBe(202); + expect(await callGateway(request, g.urlName, bearer(login.token({ aud: "bitween-e2e", sub: identity })))).toBe(401); + } finally { + await login.close(); + } + + await cleanUp(api, g); +}); + +test("renaming a gateway and changing its URL name on its page: the name survives a reload, the old URL answers 404 and the new one takes calls", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + const key = await issueKey(page, g.partnerId, "Main"); + const newName = `Playwright Renamed Gateway ${stamp()}`; + const newUrl = `${g.urlName}-v2`; + + await page.goto(`api-gateways/${g.gatewayId}`); + await page.getByRole("textbox", { name: "Name", exact: true }).fill(newName); + // Typed with capitals and a space: the field turns it into a URL name as you type. + await page.locator("#ag-url").fill(`${g.urlName} V2`); + await expect(page.locator("#ag-url")).toHaveValue(newUrl); + await expect(page.getByText(`/api/gateway/${newUrl}/sync`).first()).toBeVisible(); + + await page.getByRole("button", { name: "Save changes" }).click(); + const confirm = page.getByRole("dialog", { name: "Change this gateway's URL?" }); + await expect(confirm).toContainText(`/api/gateway/${g.urlName}`); + await expect(confirm).toContainText(`/api/gateway/${newUrl}`); + await confirm.getByRole("button", { name: "Change URL" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + + await page.reload(); + await expect(page.getByRole("textbox", { name: "Name", exact: true })).toHaveValue(newName); + await expect(page.locator("#ag-url")).toHaveValue(newUrl); + + expect(await callGateway(request, g.urlName, { partnerkey: key })).toBe(404); + expect(await callGateway(request, newUrl, { partnerkey: key })).toBe(202); + + // The list knows it by its new name. + await page.goto("api-gateways"); + await expect(page.getByText(newName).first()).toBeVisible(); + + await cleanUp(api, g); +}); + +test("a URL name ending in sync is refused on the gateway page before anything is saved", async ({ page, request }) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + + await page.goto(`api-gateways/${g.gatewayId}`); + await page.locator("#ag-url").fill(`${g.urlName}/sync`); + await expect(page.getByText(`It can't end in "sync"`).first()).toBeVisible(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + + const saved = await api.get<{ urlName: string }>(`/apigateways/${g.gatewayId}`); + expect(saved.urlName).toBe(g.urlName); + + await cleanUp(api, g); +}); + +test("deactivating a gateway on its page makes partner calls answer 503 until it is activated again, and keeps its key header", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const g = await partnerWithGateway(api); + const key = await issueKey(page, g.partnerId, "Main"); + + await page.goto(`api-gateways/${g.gatewayId}`); + await page.fill("#ag-key-header", "X-Acme-Key"); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + await page.getByRole("button", { name: "Deactivate" }).click(); + await page.getByRole("dialog", { name: /^Deactivate / }).getByRole("button", { name: "Deactivate" }).click(); + await expect(page.getByText("Deactivated", { exact: true })).toBeVisible(); + expect(await callGateway(request, g.urlName, { "X-Acme-Key": key })).toBe(503); + + await page.getByRole("button", { name: "Activate" }).click(); + await page.getByRole("dialog", { name: /^Activate / }).getByRole("button", { name: "Activate" }).click(); + await expect(page.getByText("Deactivated", { exact: true })).toHaveCount(0); + expect(await callGateway(request, g.urlName, { "X-Acme-Key": key })).toBe(202); + + // Switching it off and on sends only the name, URL and state: the header has to survive that. + await page.reload(); + await expect(page.locator("#ag-key-header")).toHaveValue("X-Acme-Key"); + + await cleanUp(api, g); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts index 86acc30d..f437a5e7 100644 --- a/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts @@ -1,18 +1,12 @@ import { test, expect } from "@playwright/test"; -import { pickOption } from "./helpers"; +import { pickOption, signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; /** A seeded partner, used for the attachment this test makes and then removes. */ -const PARTNER = "Acme Retail"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +const PARTNER = SEED.partner; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("API gateway: create, attach partner, create subscription detour, edit attachment, detach, delete", async ({ @@ -27,7 +21,7 @@ test("API gateway: create, attach partner, create subscription detour, edit atta await expect(page.getByRole("heading", { name })).toBeVisible(); // Attach a partner, detouring to create the required GatewayApiCall subscription. The - // detour is a nested route off the attach page, and both pickers are comboboxes now — + // detour is a nested route off the attach page, and both pickers are comboboxes now - // there is no wizard to "Continue" through. await page.getByRole("button", { name: "Attach partner" }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach$/); @@ -40,8 +34,8 @@ test("API gateway: create, attach partner, create subscription detour, edit atta // The picked partner rides along as a query param through the detour. await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach\/new-subscription/); await page.fill("#ngi-name", subscriptionName); - await pickOption(page, "Information type", /Shipment order/); - await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(/Shipment order/); + await pickOption(page, "Information type", new RegExp(SEED.informationType)); + await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(new RegExp(SEED.informationType)); await pickOption(page, "handler adapter", "NativeHttpHandler"); await page.locator("#prop-Url").fill("https://example.com/sink"); await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/sink"); @@ -57,7 +51,7 @@ test("API gateway: create, attach partner, create subscription detour, edit atta await expect(page.getByText(PARTNER).first()).toBeVisible(); await expect(page.getByText(subscriptionName).first()).toBeVisible(); - // Edit the attachment — exercises the remove-then-add path (backend's + // Edit the attachment - exercises the remove-then-add path (backend's // updatepartner can't mutate a composite-key column in place). await page.getByRole("button", { name: `Edit attachment for ${PARTNER}` }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+\/attachments\/\d+$/); @@ -74,7 +68,7 @@ test("API gateway: create, attach partner, create subscription detour, edit atta await expect(page.getByRole("dialog")).toHaveCount(0); await expect(page.getByText("No partners attached")).toBeVisible(); - // Delete (no attachments left — exercises the plain path; cascade-delete + // Delete (no attachments left - exercises the plain path; cascade-delete // path is covered separately since seed data already has an attached gateway). await page.getByRole("button", { name: "Delete" }).click(); await page @@ -91,13 +85,13 @@ test("Bus gateway: create, add route with match expression, edit route, remove, await page.goto("bus-gateways/new"); await page.fill("#nbg-name", name); // Bus-enabled types only, and this one is the one no seeded gateway already listens for. - await pickOption(page, "Information type", /Delivery proof/); - await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(/Delivery proof/); + await pickOption(page, "Information type", new RegExp(SEED.busInformationType)); + await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(new RegExp(SEED.busInformationType)); await page.getByRole("button", { name: "Create gateway" }).click(); await expect(page).toHaveURL(/\/bus-gateways\/\d+$/); await expect(page.getByRole("heading", { name })).toBeVisible(); - // Routes are built on the gateway's own canvas now — no separate add-route page, and no + // Routes are built on the gateway's own canvas now - no separate add-route page, and no // wizard: the route, its subscription and that subscription's delivery are all edited in // place, and "Create route" saves the lot. // An empty gateway offers both "Add a route" in the header and "Add the first route" @@ -105,7 +99,14 @@ test("Bus gateway: create, add route with match expression, edit route, remove, await page.getByRole("button", { name: /^Add (a|the first) route/ }).first().click(); await expect(page).toHaveURL(/\/bus-gateways\/\d+\?route=new/); - // No partner and no filter: an empty match expression means "matches everything". + // A new route opens on its own node, which is where its filter lives. Filters match on the + // information type's promoted properties, which the seed gives this type. + const { path } = SEED.busFilterProperty; + await page.getByRole("button", { name: "Add a filter" }).click(); + await page.getByRole("combobox", { name: "Property" }).selectOption(path); + await page.getByRole("textbox", { name: "Values" }).fill("DHL"); + + // No partner: the route runs without {{partner.…}} values. const subscriptionName = `Playwright Bus Subscription ${Date.now()}`; await page.getByRole("button", { name: "New subscription" }).click(); await page.fill("#bs-int-name", subscriptionName); @@ -119,9 +120,24 @@ test("Bus gateway: create, add route with match expression, edit route, remove, await page.getByRole("button", { name: "Create route" }).click(); await expect(page).toHaveURL(/\/bus-gateways\/\d+\?.*route=\d+/); - // Reload to prove the route round-tripped, null match expression and all. + // Reload to prove the route round-tripped, filter and all. The route list names each route by + // its filter, so that is where the saved expression shows. await page.reload(); await expect(page.getByText(subscriptionName).first()).toBeVisible(); + await expect(page.getByText(`${path} is DHL`).first()).toBeVisible(); + + // Edit the route: widen the filter to a second value. This goes through the route update + // rather than the create, which is the path that has to carry the expression over intact. + await page.getByRole("button", { name: /^Matches when/ }).first().click(); + await expect(page.getByRole("textbox", { name: "Values" })).toHaveValue("DHL"); + await page.getByRole("textbox", { name: "Values" }).fill("DHL, UPS"); + await expect(page.getByText(/Unsaved: the route \(filter\)/)).toBeVisible(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByText(/^Unsaved:/)).toHaveCount(0); + + await page.reload(); + await expect(page.getByText(`${path} is one of DHL, UPS`).first()).toBeVisible(); + await expect(page.getByText(`${path} is DHL`, { exact: true })).toHaveCount(0); // Remove the route. await page.getByRole("button", { name: "Remove route" }).click(); @@ -131,9 +147,9 @@ test("Bus gateway: create, add route with match expression, edit route, remove, .click(); await expect(page.getByRole("dialog")).toHaveCount(0); // The route list and the main panel both say this, so name the panel's heading: it shows only - // when no route is selected, which is the thing actually worth proving here — that removing + // when no route is selected, which is the thing actually worth proving here - that removing // the last route doesn't leave the page still pointing at it. - await expect(page.getByRole("heading", { name: /^No routes —/ })).toBeVisible(); + await expect(page.getByRole("heading", { name: /^No routes -/ })).toBeVisible(); await page.getByRole("button", { name: "Delete" }).click(); await page diff --git a/SW.Bitween.Web/ClientApp/e2e/global-setup.ts b/SW.Bitween.Web/ClientApp/e2e/global-setup.ts deleted file mode 100644 index 8637afd2..00000000 --- a/SW.Bitween.Web/ClientApp/e2e/global-setup.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { request } from "@playwright/test"; - -/** - * Leaves the database in a known state before the suite runs. - * - * A test that fails part-way skips its own cleanup, and the leftovers aren't harmless: a stray - * account still holding Administrator is enough to make the last-administrator guard pass, which - * strips the seeded admin's role and fails everything after it. So rather than trusting each test - * to tidy up, every run starts by purging its own residue and putting the admin's role back. - */ - -const API = "https://localhost:7155/api"; -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; -const ADMINISTRATOR_ROLE_ID = 1; - -const TEST_EMAIL = /^pw-.*@example\.test$/; -const TEST_ROLE = /^PW /; -/** Everything the suite creates is named this way, so it can be found again and removed. */ -const TEST_NAME = /^Playwright /; -/** The only settings the suite writes to — see the reset below for why this is a list, not "all". */ -const TEST_SETTINGS = ["Theme.PrimaryColor", "Theme.TabTitle", "Theme.CompanyName"]; - -interface Account { - id: number; - email: string; - roles: { id: number; name: string }[] | null; -} - -export default async function purgeTestData() { - const api = await request.newContext({ ignoreHTTPSErrors: true }); - - const login = await api.post(`${API}/accounts/login`, { - data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, - }); - let token: string = login.ok() ? (await login.json()).jwt : ""; - - const auth = () => ({ Authorization: `Bearer ${token}` }); - - // The seeded admin ships with a published password, so a freshly migrated database flags it and - // issues it a token that grants nothing until someone chooses a new one. Setting it back to the - // same value clears the flag, which is the whole of what the suite needs: every spec signs in as - // this account, and none of them is about the forced change itself. - // - // Deliberately loud about what it is. Re-setting a known password is exactly the thing the flag - // exists to prevent — it is defensible here only because this database exists to be thrown away. - await api.post(`${API}/accounts/changePassword`, { - headers: auth(), - data: { OldPassword: ADMIN_PASSWORD, NewPassword: ADMIN_PASSWORD }, - }); - - const accounts = await api.get(`${API}/accounts?limit=500`, { headers: auth() }); - - if (!accounts.ok()) { - // There used to be a fallback here: a second sign-in against credentials held in - // configuration, which worked even for an admin holding no roles. That endpoint shipped with a - // published default password and has been removed, so this is now the only way in — and if the - // seeded admin has lost Administrator, no request can put it back. - throw new Error( - "Can't reach the API as the seeded administrator. Either the local backend isn't running, " + - "or the admin has lost its Administrator role and the database needs restoring.", - ); - } - - const rows: Account[] = (await accounts.json()).result ?? []; - - // The seeded admin must hold Administrator, or nothing downstream can manage anything. - const admin = rows.find((a) => a.email.toLowerCase() === ADMIN_EMAIL.toLowerCase()); - if (admin && !(admin.roles ?? []).some((r) => r.id === ADMINISTRATOR_ROLE_ID)) { - await api.post(`${API}/accounts/${admin.id}/setRoles`, { - headers: auth(), - data: { roleIds: [ADMINISTRATOR_ROLE_ID] }, - }); - // Re-read as the repaired admin so the purge below runs with full permissions. - const relogin = await api.post(`${API}/accounts/login`, { - data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, - }); - if (relogin.ok()) token = (await relogin.json()).jwt; - } - - for (const account of rows.filter((a) => TEST_EMAIL.test(a.email))) - await api.post(`${API}/accounts/${account.id}/remove`, { headers: auth(), data: {} }); - - const roles = await api.get(`${API}/roles?pageSize=500`, { headers: auth() }); - for (const role of ((await roles.json()).result ?? []) as { id: number; name: string }[]) - if (TEST_ROLE.test(role.name)) - await api.delete(`${API}/roles/${role.id}`, { headers: auth() }); - - // Settings tests assert against product defaults, so a value left behind by a failed run would - // make them fail for the wrong reason. Only the keys the tests actually touch are reset: the - // Settings table is now the only home for values like the MSAL ids and the Rebex license key — - // configuration is read once at first boot and ignored after that — so a blanket reset here - // would destroy real configuration with no way to get it back. - for (const key of TEST_SETTINGS) - await api.delete(`${API}/settings/${encodeURIComponent(key)}`, { headers: auth() }); - - // The domain objects the suite creates outlive a failed run too, and they are not harmless - // either: enough of them push a newly created row off the first page of a list, and the tests - // that look for their own row then fail for a reason that has nothing to do with them. - // - // Order matters. A gateway holds attachments and routes that reference subscriptions, so the - // gateways go first or the subscriptions underneath them refuse to delete. - const purge = async (list: string, remove: (id: number) => Promise) => { - const res = await api.get(`${API}/${list}?size=500&limit=500`, { headers: auth() }); - if (!res.ok()) return; - const rows = ((await res.json()).result ?? []) as { id: number; name: string }[]; - for (const row of rows.filter((r) => TEST_NAME.test(r.name ?? ""))) { - // Best effort: something still referencing a row is not a reason to abandon the rest. - try { - await remove(row.id); - } catch { - /* leave it for the next run */ - } - } - }; - - await purge("apigateways", (id) => api.delete(`${API}/apigateways/${id}`, { headers: auth() })); - await purge("busgateways", (id) => api.delete(`${API}/busgateways/${id}`, { headers: auth() })); - await purge("subscriptions", (id) => api.delete(`${API}/subscriptions/${id}`, { headers: auth() })); - await purge("workgroups", (id) => - api.post(`${API}/workgroups/${id}/delete`, { headers: auth(), data: {} }), - ); - await purge("documents", (id) => api.delete(`${API}/documents/${id}`, { headers: auth() })); - await purge("partners", (id) => api.delete(`${API}/partners/${id}`, { headers: auth() })); - - // Values sets are keyed by a string id rather than a number, so `purge` cannot do them. - const sets = await api.get(`${API}/globaladaptervaluessets?size=500&limit=500`, { headers: auth() }); - if (sets.ok()) - for (const set of ((await sets.json()).result ?? []) as { id: string; name: string }[]) - if (TEST_NAME.test(set.name ?? "")) - await api.post(`${API}/globaladaptervaluessets/${set.id}/delete`, { - headers: auth(), - data: {}, - }); - - await api.dispose(); -} diff --git a/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts b/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts index 91137fc6..b263eb84 100644 --- a/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/global-values.spec.ts @@ -1,14 +1,8 @@ import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("global value set create, edit values, list, delete", async ({ page }) => { @@ -16,7 +10,7 @@ test("global value set create, edit values, list, delete", async ({ page }) => { const name = `Playwright Values ${stamp}`; await page.goto("global-values"); - await page.getByRole("button", { name: "New value set" }).click(); + await page.getByRole("button", { name: "New value set" }).first().click(); await page.fill("#nvs-name", name); await page.getByRole("button", { name: "Create value set" }).click(); @@ -29,7 +23,7 @@ test("global value set create, edit values, list, delete", async ({ page }) => { await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); - // Reload forces a fresh GET — proves the write actually persisted server-side. + // Reload forces a fresh GET - proves the write actually persisted server-side. await page.reload(); await expect(page.getByLabel("Key 1")).toHaveValue("baseUrl"); await expect(page.getByLabel("Value 1")).toHaveValue("https://example.com"); diff --git a/SW.Bitween.Web/ClientApp/e2e/helpers.ts b/SW.Bitween.Web/ClientApp/e2e/helpers.ts index 6b52ae10..6d6261f0 100644 --- a/SW.Bitween.Web/ClientApp/e2e/helpers.ts +++ b/SW.Bitween.Web/ClientApp/e2e/helpers.ts @@ -1,13 +1,20 @@ import { type Page } from "@playwright/test"; +import { ADMIN_EMAIL, ADMIN_PASSWORD } from "./env"; /** Checkbox labels carry their description in the accessible name, so anchor at the start. */ export const startsWith = (text: string) => new RegExp("^" + text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); -export const ADMIN_EMAIL = "admin@Bitween.systems"; -export const ADMIN_PASSWORD = "Mtm@dmin!2"; +/** Who the suite signs in as and where - read from the environment, see env.ts. */ +export { ADMIN_EMAIL, ADMIN_PASSWORD, API } from "./env"; /** Passwords the members these tests create are given. Both clear the 8-character minimum. */ +/** + * A subscription's own page. Scheduled jobs, aggregations and response subscriptions open in their + * own section (/scheduled-jobs/:id …), everything else at /subscriptions/:id. + */ +export const SUBSCRIPTION_PAGE = /\/(subscriptions|scheduled-jobs|aggregations|response-subscriptions)\/\d+$/; + export const FIRST_PASSWORD = "Pl4ywright!1"; export const ROTATED_PASSWORD = "R0tated!Pass2"; @@ -15,14 +22,14 @@ export const ROTATED_PASSWORD = "R0tated!Pass2"; * Choose an option from a SearchSelect, and leave the page interactive. * * SearchSelect is a Headless UI `Combobox` with `immediate`, so it opens its - * listbox whenever the input takes focus — and Headless UI restores focus to + * listbox whenever the input takes focus - and Headless UI restores focus to * that input a few milliseconds after the list unmounts. So every close * schedules a reopen, and a test that moves on inside that window gets its * focus stolen and then wedged: an open listbox marks the rest of the page * `inert`, and an inert page can never be clicked to dismiss the listbox. The * failure reads as "
intercepts pointer events" and never recovers. * - * A person never sees this — their next action is far slower than the restore, + * A person never sees this - their next action is far slower than the restore, * which is why the app behaves correctly by hand. This is the test catching up * with the UI, so it dismisses the list until it stays shut rather than * assuming one Escape is enough. @@ -126,3 +133,21 @@ export async function deleteRole(page: Page, name: string) { await page.getByRole("button", { name: "Delete role" }).last().click(); await page.waitForURL(/\/team\/roles$/, { timeout: 15000 }); } + +/** + * A Jwt for the signed-in page's own session, for specs that call the API directly. The app keeps + * its Jwt in memory where a spec cannot reach it, so this asks the server for one with the refresh + * cookie the page holds - the same thing a reload does. + */ +export async function sessionToken(page: Page): Promise { + return page.evaluate(async () => { + const res = await fetch("/api/accounts/login", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: "{}", + }); + const data = (await res.json()) as { jwt?: string; Jwt?: string }; + return data.jwt ?? data.Jwt ?? ""; + }); +} diff --git a/SW.Bitween.Web/ClientApp/e2e/http-login.spec.ts b/SW.Bitween.Web/ClientApp/e2e/http-login.spec.ts index efecd4e5..bf0a9640 100644 --- a/SW.Bitween.Web/ClientApp/e2e/http-login.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/http-login.spec.ts @@ -1,7 +1,8 @@ import { createServer, type IncomingMessage, type Server } from "node:http"; import type { AddressInfo } from "node:net"; import { test, expect, type Page } from "@playwright/test"; -import { pickOption, signInAsAdmin } from "./helpers"; +import { pickOption, signInAsAdmin, SUBSCRIPTION_PAGE } from "./helpers"; +import { SEED } from "./seed-data"; /** * The HTTP adapters' "Login" auth, against a partner API that logs in its own way: it wants @@ -74,7 +75,7 @@ test("HTTP receiver and handler log in with a custom body and token path", async await page.goto("scheduled-jobs/new"); await page.fill("#nj-name", name); - await pickOption(page, "Information type", /Shipment order/); + await pickOption(page, "Information type", new RegExp(SEED.informationType)); await pickOption(page, "receiver adapter", "NativeHttpReceiver"); await page.locator("#prop-Url").fill(`${base}/feed`); @@ -87,7 +88,7 @@ test("HTTP receiver and handler log in with a custom body and token path", async await fillLogin(page); await page.getByRole("button", { name: "Create job" }).click(); - await expect(page).toHaveURL(/\/subscriptions\/\d+$/); + await expect(page).toHaveURL(SUBSCRIPTION_PAGE); await page.getByRole("button", { name: "Receive now" }).click(); await page.getByRole("dialog", { name: "Receive now?" }).getByRole("button", { name: "Receive now" }).click(); diff --git a/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts b/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts index 4481463a..0043e69b 100644 --- a/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/information-types.spec.ts @@ -1,14 +1,8 @@ import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("information type Code is optional end to end", async ({ page }) => { @@ -16,16 +10,16 @@ test("information type Code is optional end to end", async ({ page }) => { // Creating happens in a dialog on the list page, not on a page of its own. await page.goto("information-types"); - await page.getByRole("button", { name: "New information type" }).click(); + await page.getByRole("button", { name: "New information type" }).first().click(); const dialog = page.getByRole("dialog", { name: "New information type" }); await dialog.getByRole("textbox", { name: "Name" }).fill(name); - // Code starts empty and stays optional — nothing to clear, and it doesn't block creating. + // Code starts empty and stays optional - nothing to clear, and it doesn't block creating. await expect(dialog.getByRole("textbox", { name: "Code" })).toHaveValue(""); await dialog.getByRole("button", { name: "Create information type" }).click(); - // Should navigate straight to the detail page — no validation block on empty code. + // Should navigate straight to the detail page - no validation block on empty code. await expect(page).toHaveURL(/\/information-types\/\d+$/); await expect(page.getByRole("heading", { name })).toBeVisible(); await expect(page.locator("#it-code")).toHaveValue(""); diff --git a/SW.Bitween.Web/ClientApp/e2e/legacy-mapper.spec.ts b/SW.Bitween.Web/ClientApp/e2e/legacy-mapper.spec.ts new file mode 100644 index 00000000..d4d8fbd0 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/legacy-mapper.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from "@playwright/test"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { AdminApi, stamp } from "./api"; +import { signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * The legacy JSON mapper (NativeJSONMapper) and its own editor. New subscriptions can't pick it any + * more, but the ones already on it keep it and keep this editor, so it still has to open their + * mapping, save it, and - what matters to them - run it. The subscription is made through the + * API because the picker no longer offers the mapper; everything after that is the editor. + */ + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("the legacy JSON mapper's editor maps by auto-match, previews through the server, saves, reopens with the mapping, and the saved mapping shapes a real delivery", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const s = stamp(); + + const received: string[] = []; + const endpoint = createServer((req, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + received.push(body); + res.end("ok"); + }); + }); + await new Promise((resolve) => endpoint.listen(0, "127.0.0.1", resolve)); + const url = `http://127.0.0.1:${(endpoint.address() as AddressInfo).port}/`; + + let id: number | null = null; + try { + id = await api.post("/subscriptions", { + name: `Playwright Legacy Mapping ${s}`, + documentId: await api.idOf("documents", SEED.informationType), + type: "ApiCall", + partnerId: await api.idOf("partners", SEED.partner), + mapperId: "NativeJSONMapper", + // What a subscription on this mapper has as a minimum: a template, here an empty object. + mapperProperties: [{ key: "ScribanTemplate", value: "{}" }], + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: url }], + receiverProperties: [], + validatorProperties: [], + documentFilter: [], + inactive: false, + }); + + await page.goto(`subscriptions/${id}/mapper`); + // The old editor, not the native mapper's: it is chosen by the mapper the subscription runs. + await expect(page.getByText("Mapping Editor", { exact: true })).toBeVisible({ timeout: 15000 }); + + await page.getByPlaceholder('{ "paste": "source JSON here" }').fill( + JSON.stringify({ orderId: "A-1", customer: "Acme", internalNote: "drop me" }), + ); + await page.getByPlaceholder(/output shape/).fill(JSON.stringify({ orderId: "", customer: "" })); + await page.getByRole("button", { name: "Generate from JSON" }).click(); + await page.getByRole("button", { name: "Auto-match" }).click(); + await expect(page.getByText("2 mappings ·")).toBeVisible(); + + await page.getByRole("button", { name: /Show Live Preview/ }).click(); + const preview = page.locator("pre").last(); + await expect(preview).toContainText('"orderId"', { timeout: 15000 }); + await expect(preview).toContainText("A-1"); + await expect(preview).toContainText("Acme"); + await expect(preview).not.toContainText("drop me"); + + await page.getByRole("button", { name: "Save", exact: true }).click(); + await expect(page.getByText("✓ Saved")).toBeVisible(); + + // Reopened from scratch, the editor rebuilds the same mapping from what was saved. + await page.reload(); + await expect(page.getByText("2 mappings ·")).toBeVisible({ timeout: 15000 }); + await expect(page.getByPlaceholder('{ "paste": "source JSON here" }')).toHaveValue(/A-1/); + + const raw = await api.get<{ mapperId: string; mapperProperties: { key: string; value: string }[] | Record }>( + `/subscriptions/${id}`, + ); + expect(raw.mapperId).toBe("NativeJSONMapper"); + const props = Array.isArray(raw.mapperProperties) + ? Object.fromEntries(raw.mapperProperties.map((p) => [p.key, p.value])) + : raw.mapperProperties; + expect(props.ScribanTemplate).toContain("orderId"); + + // And it runs: a real exchange is delivered in the mapped shape. + await api.sendExchange(id, { orderId: `B-${s}`, customer: "Zeta", internalNote: "secret" }); + await expect + .poll(() => received.length, { message: "the mapped document never arrived", timeout: 60_000 }) + .toBe(1); + expect(JSON.parse(received[0])).toEqual({ orderId: `B-${s}`, customer: "Zeta" }); + } finally { + await new Promise((resolve) => endpoint.close(() => resolve())); + if (id !== null) await api.delete(`/subscriptions/${id}`); + } +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/login.spec.ts b/SW.Bitween.Web/ClientApp/e2e/login.spec.ts index 083cfb15..98c641c7 100644 --- a/SW.Bitween.Web/ClientApp/e2e/login.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/login.spec.ts @@ -4,7 +4,7 @@ import { ADMIN_EMAIL, ADMIN_PASSWORD, signInAsAdmin } from "./helpers"; /** * Where signing in leaves you. Two different answers, which is the point of testing both: an * operator opening the app wants to see how the system is doing, but somebody who followed a - * link — or whose token expired mid-task — wants the page they were going to, not a detour. + * link - or whose token expired mid-task - wants the page they were going to, not a detour. */ test("signing in with nowhere to go lands on the dashboard", async ({ page }) => { await signInAsAdmin(page); @@ -14,7 +14,7 @@ test("signing in with nowhere to go lands on the dashboard", async ({ page }) => }); test("signing in to reach a page lands on that page, not the dashboard", async ({ page }) => { - // Arriving cold at a protected URL — a link from a colleague, or a reload after the token + // Arriving cold at a protected URL - a link from a colleague, or a reload after the token // expired. The guard sends it to login and login has to give it back. await page.goto("partners"); await page.waitForURL((url) => url.pathname.endsWith("/login")); diff --git a/SW.Bitween.Web/ClientApp/e2e/mapper-xml.spec.ts b/SW.Bitween.Web/ClientApp/e2e/mapper-xml.spec.ts index a95fe104..e78e3af7 100644 --- a/SW.Bitween.Web/ClientApp/e2e/mapper-xml.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/mapper-xml.spec.ts @@ -18,7 +18,7 @@ import { * path out of the tree and then asserts on a preview the server produced, so the two * cannot drift apart without one of them failing. * - * The documents are the shapes real carriers send — SOAP requests and responses — with + * The documents are the shapes real carriers send - SOAP requests and responses - with * the credentials replaced. */ @@ -75,6 +75,6 @@ test("the paths the source tree offers are the ones the server can read", async await field.fill("Envelope.Body.shipping.headerValue.accountNumber"); - // Read by the server, not by the editor — which is what makes this a real check. + // Read by the server, not by the editor - which is what makes this a real check. await expectPreview(page, '"account": "55480501"'); }); diff --git a/SW.Bitween.Web/ClientApp/e2e/mapperHelpers.ts b/SW.Bitween.Web/ClientApp/e2e/mapperHelpers.ts index 86e7a2e6..6b937165 100644 --- a/SW.Bitween.Web/ClientApp/e2e/mapperHelpers.ts +++ b/SW.Bitween.Web/ClientApp/e2e/mapperHelpers.ts @@ -1,15 +1,14 @@ import { expect, request, type APIRequestContext, type Locator, type Page } from "@playwright/test"; -import { ADMIN_EMAIL, ADMIN_PASSWORD, pickOption } from "./helpers"; +import { ADMIN_EMAIL, ADMIN_PASSWORD, API, pickOption, SUBSCRIPTION_PAGE } from "./helpers"; +import { SEED } from "./seed-data"; /** * Driving the new mapping editor. * * Shared by both mapper specs so a change to the editor's chrome is one edit here - * rather than one per test — the specs then read as the mapping they are about. + * rather than one per test - the specs then read as the mapping they are about. */ -const API = "https://localhost:7155/api"; - /** A source document with a value, a number, and a list with one entry to filter out. */ export const SAMPLE = JSON.stringify( { @@ -32,7 +31,7 @@ export async function createSubscription(page: Page): Promise { await page.goto("scheduled-jobs/new"); await page.fill("#nj-name", name); - await pickOption(page, "Information type", /Shipment order/); + await pickOption(page, "Information type", SEED.informationType); await pickOption(page, "receiver adapter", "NativeHttpReceiver"); await page.locator("#prop-Url").fill("https://example.com/feed"); @@ -43,9 +42,9 @@ export async function createSubscription(page: Page): Promise { await page.locator("#prop-Url").fill("https://example.com/post"); await page.getByRole("button", { name: /^(Create|Save)/ }).last().click(); - await page.waitForURL(/\/subscriptions\/\d+/, { timeout: 15000 }); + await page.waitForURL(SUBSCRIPTION_PAGE, { timeout: 15000 }); - return page.url().match(/\/subscriptions\/(\d+)/)![1]; + return page.url().match(/\/(\d+)$/)![1]; } export async function openMapper(page: Page, subscriptionId: string) { @@ -57,7 +56,7 @@ export async function openMapper(page: Page, subscriptionId: string) { * Runs `set` with the format panel open, and closes it afterwards. * * What the mapping reads and writes sits behind a summary chip rather than in the - * toolbar row, so a test that changes a format has to open the panel first — and the + * toolbar row, so a test that changes a format has to open the panel first - and the * panel covers the rules underneath, so it has to be closed before touching them. */ export async function withFormats(page: Page, set: () => Promise) { @@ -137,7 +136,7 @@ async function adminApi(): Promise { data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, }); // Without this the token becomes the string "undefined", and every later call fails - // as "could not read subscription " — which blames the subscription, not the login. + // as "could not read subscription " - which blames the subscription, not the login. if (!login.ok()) throw new Error(`could not sign in as ${ADMIN_EMAIL}: ${login.status()} ${await login.text()}`); token = (await login.json()).jwt; diff --git a/SW.Bitween.Web/ClientApp/e2e/member-access.spec.ts b/SW.Bitween.Web/ClientApp/e2e/member-access.spec.ts new file mode 100644 index 00000000..565acaf2 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/member-access.spec.ts @@ -0,0 +1,421 @@ +import { test, expect, type Page } from "@playwright/test"; +import { AdminApi } from "./api"; +import { + ADMIN_EMAIL, + API, + FIRST_PASSWORD, + ROTATED_PASSWORD, + addMember, + createRole, + deleteRole, + openMember, + removeMember, + sessionToken, + signIn, + signInAsAdmin, + signOut, + startsWith, +} from "./helpers"; + +/** + * What an administrator does to a member after adding them - change their roles, disable them, + * unlock them - and the guards around it: the last administrator can't be demoted, and a member + * who manages members can't hand out more than they hold. Every change is followed to its effect: + * the member signing in (or not), and the server's own record of their roles. + */ + +const drawer = (page: Page) => page.getByRole("dialog", { name: "Member details" }); + +/** Submits the login form and leaves the page wherever that lands. */ +async function trySignIn(page: Page, email: string, password: string) { + await page.goto("login"); + await page.fill("#login-email", email); + await page.fill("#login-password", password); + await page.getByRole("button", { name: "Sign in" }).click(); +} + +const sidebarLink = (page: Page, text: string) => + page.getByRole("navigation").getByRole("link").filter({ hasText: text }); + +async function roleNamesOf(api: AdminApi, email: string) { + return ((await api.account(email)).roles ?? []).map((r) => r.name).sort(); +} + +test("a custom role given to an existing member in the drawer lets them create a partner, but not delete it or open pages outside the role", async ({ + page, + request, +}) => { + const roleName = `PW Partner Clerk ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [ + { area: "Partners", action: "View" }, + { area: "Partners", action: "Create" }, + ], + }); + // They start with Viewer, which can read partners but not create them. + const email = await addMember(page, { name: "Becomes Clerk", roles: ["Viewer"] }); + + await openMember(page, email); + await drawer(page).getByRole("checkbox", { name: startsWith("Viewer") }).uncheck(); + await drawer(page).getByRole("checkbox", { name: startsWith(roleName) }).check(); + await drawer(page).getByRole("button", { name: "Save roles" }).click(); + await expect(drawer(page).getByRole("button", { name: "Save roles" })).toHaveCount(0); + + // The list says so, and so does the server. + await page.keyboard.press("Escape"); + const row = page.getByRole("row", { name: new RegExp(email) }); + await expect(row).toContainText(roleName); + await expect(row).not.toContainText("Viewer"); + const api = await AdminApi.signIn(request); + expect(await roleNamesOf(api, email)).toEqual([roleName]); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + // Partners: they can open the list and create one. + await expect(sidebarLink(page, "Partners")).toBeVisible(); + // Viewer's pages are gone with the role. + for (const hidden of ["Exchanges", "Subscriptions", "Team"]) + await expect(sidebarLink(page, hidden)).toHaveCount(0); + await page.goto("subscriptions"); + await expect(page.getByText("You don't have access to this page")).toBeVisible(); + + await page.goto("partners"); + await page.getByRole("button", { name: "New partner" }).first().click(); + const partnerName = `Playwright Clerk Partner ${Date.now()}`; + const dialog = page.getByRole("dialog", { name: "New partner" }); + await dialog.locator("#pf-name").fill(partnerName); + await dialog.getByRole("button", { name: "Create partner" }).click(); + await expect(page).toHaveURL(/\/partners\/\d+$/); + await expect(page.getByText(partnerName).first()).toBeVisible(); + + // But not delete it: no button, and the API refuses the request the button would have sent. + const partnerId = await api.idOf("partners", partnerName); + await page.goto(`partners/${partnerId}`); + await expect(page.getByRole("heading", { level: 1 })).toContainText(partnerName); + await expect(page.getByRole("button", { name: "Delete partner" })).toHaveCount(0); + const token = await sessionToken(page); + const del = await page.request.delete(`${API}/partners/${partnerId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(del.status()).toBe(403); + + await signOut(page); + await signInAsAdmin(page); + await api.delete(`/partners/${partnerId}`); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("a role with Create but not Edit can create an information type and a work group from their New dialogs and open what it made", async ({ + page, + request, +}) => { + const roleName = `PW Creator ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [ + { area: "Information types", action: "View" }, + { area: "Information types", action: "Create" }, + { area: "Work groups", action: "View" }, + { area: "Work groups", action: "Create" }, + ], + }); + const email = await addMember(page, { name: "Create Only", roles: [roleName] }); + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + + const typeName = `Playwright Created Type ${Date.now()}`; + await page.goto("information-types"); + await page.getByRole("button", { name: "New information type" }).first().click(); + const typeDialog = page.getByRole("dialog", { name: "New information type" }); + await typeDialog.getByRole("textbox", { name: "Name" }).fill(typeName); + await typeDialog.getByRole("button", { name: "Create information type" }).click(); + await expect(page).toHaveURL(/\/information-types\/\d+$/); + await expect(page.getByRole("heading", { name: typeName })).toBeVisible(); + + const groupName = `Playwright Created Group ${Date.now()}`; + await page.goto("work-groups"); + await page.getByRole("button", { name: "New work group" }).first().click(); + const groupDialog = page.getByRole("dialog", { name: "New work group" }); + await groupDialog.getByRole("textbox", { name: "Name", exact: true }).fill(groupName); + await groupDialog.getByRole("button", { name: "Create work group" }).click(); + await expect(page).toHaveURL(/\/work-groups\/\d+$/, { timeout: 15000 }); + await expect(page.getByRole("heading", { name: groupName })).toBeVisible(); + + const api = await AdminApi.signIn(request); + const typeId = await api.idOf("documents", typeName); + const groupId = await api.idOf("workgroups", groupName); + + await signOut(page); + await signInAsAdmin(page); + await api.delete(`/documents/${typeId}`); + await api.post(`/workgroups/${groupId}/delete`); + await removeMember(page, email); + await deleteRole(page, roleName); +}); + +test("changing a member's roles in the drawer replaces them: the new role's pages open, the old one's close", async ({ + page, + request, +}) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Role Swap", roles: ["Viewer"] }); + + // Viewer reads configuration but has no Team pages; Member runs integrations but no Team + // either - so the difference to look for is write access, e.g. "New partner". + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await page.goto("partners"); + await expect(page.getByRole("heading", { name: "Partners" })).toBeVisible(); + await expect(page.getByRole("button", { name: "New partner" })).toHaveCount(0); + + await signOut(page); + await signInAsAdmin(page); + await openMember(page, email); + await drawer(page).getByRole("checkbox", { name: startsWith("Viewer") }).uncheck(); + await drawer(page).getByRole("checkbox", { name: startsWith("Member") }).check(); + await drawer(page).getByRole("button", { name: "Save roles" }).click(); + await expect(drawer(page).getByRole("button", { name: "Save roles" })).toHaveCount(0); + + // Reopened from scratch, the drawer shows what the server now holds. + await page.reload(); + await expect(drawer(page).getByRole("checkbox", { name: startsWith("Member") })).toBeChecked(); + await expect(drawer(page).getByRole("checkbox", { name: startsWith("Viewer") })).not.toBeChecked(); + const api = await AdminApi.signIn(request); + expect(await roleNamesOf(api, email)).toEqual(["Member"]); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await page.goto("partners"); + await expect(page.getByRole("button", { name: "New partner" })).toBeVisible(); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("a disabled member can't sign in, loses the session they had open, and can again once re-enabled", async ({ + page, + browser, +}) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Gets Disabled", roles: ["Viewer"] }); + + // The member is signed in elsewhere when it happens. + const theirs = await browser.newContext({ ignoreHTTPSErrors: true }); + const theirPage = await theirs.newPage(); + await signIn(theirPage, email, FIRST_PASSWORD); + + await openMember(page, email); + await drawer(page).getByRole("button", { name: "Disable account" }).click(); + await expect(drawer(page).getByRole("button", { name: "Re-enable account" })).toBeVisible(); + await expect(drawer(page).getByText("Disabled", { exact: true })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByRole("row", { name: new RegExp(email) })).toContainText("Disabled"); + + // Their open session ends: a reload has to renew the Jwt, and the refresh token is gone. + await theirPage.reload(); + await theirPage.waitForURL((url) => url.pathname.endsWith("/login"), { timeout: 15000 }); + await theirs.close(); + + // And signing in again is refused, saying why. + await signOut(page); + await trySignIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("alert")).toContainText("Your account has been disabled"); + await expect(page).toHaveURL(/\/login$/); + + await signInAsAdmin(page); + await openMember(page, email); + await drawer(page).getByRole("button", { name: "Re-enable account" }).click(); + await expect(drawer(page).getByRole("button", { name: "Disable account" })).toBeVisible(); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("Gets Disabled"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("a member locked by wrong passwords from many addresses shows as Locked, is refused even with the right password, and signs in once unlocked", async ({ + page, + request, +}) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Guess Target", roles: ["Viewer"] }); + + // The account-wide lock comes after 20 wrong passwords (Login.MaxFailedLoginAttempts). From a + // single address the per-address throttle stops counting after 5, so this is guessing spread + // over twenty addresses - what the account-wide lock is there for. The app trusts the nearest + // proxy's X-Forwarded-For, which is how each attempt arrives from its own address. + for (let i = 0; i < 20; i++) { + const res = await request.post(`${API}/accounts/login`, { + headers: { "X-Forwarded-For": `198.51.100.${i + 1}` }, + data: { Username: email, Password: `wrong-${i}` }, + }); + expect(res.ok()).toBe(false); + } + + await page.goto("team/members"); + const status = page.getByRole("row", { name: new RegExp(email) }).getByRole("cell").nth(2); + await expect(status).toHaveText("Locked"); + + await signOut(page); + await trySignIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("alert")).toContainText("temporarily locked"); + await expect(page).toHaveURL(/\/login$/); + + await signInAsAdmin(page); + await openMember(page, email); + await drawer(page).getByRole("button", { name: "Unlock account" }).click(); + await expect(drawer(page).getByRole("button", { name: "Unlock account" })).toHaveCount(0); + await expect(drawer(page).getByText("Active", { exact: true })).toBeVisible(); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("Guess Target"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("the only administrator can't take Administrator off themselves: the drawer says why and the role stays", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + // The guard only bites when nobody else holds the role. The seed removes the suite's own + // leftovers, so anyone else here is a real administrator and the premise doesn't hold. + const accounts = await api.get<{ result: { email: string; disabled: boolean; roles: { id: number }[] | null }[] }>( + "/accounts?limit=500", + ); + const otherAdmins = accounts.result.filter( + (a) => a.email.toLowerCase() !== ADMIN_EMAIL.toLowerCase() && !a.disabled && (a.roles ?? []).some((r) => r.id === 1), + ); + expect(otherAdmins.map((a) => a.email), "another enabled administrator exists on this database").toEqual([]); + + await signInAsAdmin(page); + await openMember(page, ADMIN_EMAIL); + await expect(drawer(page).getByText("You", { exact: true })).toBeVisible(); + await drawer(page).getByRole("checkbox", { name: startsWith("Administrator") }).uncheck(); + await drawer(page).getByRole("checkbox", { name: startsWith("Viewer") }).check(); + await drawer(page).getByRole("button", { name: "Save roles" }).click(); + + await expect(drawer(page).getByRole("alert")).toContainText( + "This is the only member with the Administrator role. Give it to someone else first.", + ); + expect(await roleNamesOf(api, ADMIN_EMAIL)).toEqual(["Administrator"]); + + // Nothing was half-applied either: after a reload the drawer still shows Administrator alone. + await page.reload(); + await expect(drawer(page).getByRole("checkbox", { name: startsWith("Administrator") })).toBeChecked(); + await expect(drawer(page).getByRole("checkbox", { name: startsWith("Viewer") })).not.toBeChecked(); +}); + +test("a member who can edit members but isn't an administrator can't grant Administrator, to themselves or a peer", async ({ + page, + request, +}) => { + const roleName = `PW Member Manager ${Date.now()}`; + await signInAsAdmin(page); + await createRole(page, { + name: roleName, + permissions: [ + { area: "Members", action: "View" }, + { area: "Members", action: "Edit" }, + // The drawer lists the roles to pick from, which reading them needs. + { area: "Roles", action: "View" }, + ], + }); + const manager = await addMember(page, { name: "Member Manager", roles: [roleName] }); + // A peer holding no more than the manager does, so the manager is allowed to edit them at all. + const peer = await addMember(page, { name: "Managed Peer", roles: [roleName] }); + + await signOut(page); + await signIn(page, manager, FIRST_PASSWORD); + + for (const target of [peer, manager]) { + await openMember(page, target); + await drawer(page).getByRole("checkbox", { name: startsWith("Administrator") }).check(); + await drawer(page).getByRole("button", { name: "Save roles" }).click(); + await expect(drawer(page).getByRole("alert")).toContainText("You can only grant permissions you hold yourself"); + await page.keyboard.press("Escape"); + await drawer(page).waitFor({ state: "detached" }); + } + + const api = await AdminApi.signIn(request); + expect(await roleNamesOf(api, peer)).toEqual([roleName]); + expect(await roleNamesOf(api, manager)).toEqual([roleName]); + + // What they hold themselves they can still hand out: the guard is about the excess, not the act. + await openMember(page, peer); + await drawer(page).getByRole("checkbox", { name: startsWith(roleName) }).uncheck(); + await drawer(page).getByRole("button", { name: "Save roles" }).click(); + await expect(drawer(page).getByRole("button", { name: "Save roles" })).toHaveCount(0); + expect(await roleNamesOf(api, peer)).toEqual([]); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, peer); + await removeMember(page, manager); + await deleteRole(page, roleName); +}); + +test("changing your own password on the profile page: the old one stops working, the new one signs in", async ({ + page, +}) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Own Password", roles: ["Viewer"] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await page.goto("profile"); + await page.fill("#pf-current", FIRST_PASSWORD); + await page.fill("#pf-new", ROTATED_PASSWORD); + await page.fill("#pf-confirm", ROTATED_PASSWORD); + await page.getByRole("button", { name: "Change password" }).click(); + await expect(page.getByText("Saved", { exact: true })).toBeVisible(); + + await signOut(page); + await trySignIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("alert")).toContainText("Invalid username or password"); + await expect(page).toHaveURL(/\/login$/); + + await signIn(page, email, ROTATED_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("Own Password"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); + +test("the profile page refuses a password change when the current password is wrong, and the old one keeps working", async ({ + page, +}) => { + await signInAsAdmin(page); + const email = await addMember(page, { name: "Wrong Current", roles: ["Viewer"] }); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await page.goto("profile"); + await page.fill("#pf-current", "not-my-password-1A!"); + await page.fill("#pf-new", ROTATED_PASSWORD); + await page.fill("#pf-confirm", ROTATED_PASSWORD); + await page.getByRole("button", { name: "Change password" }).click(); + await expect(page.getByRole("alert")).toBeVisible(); + await expect(page.getByText("Saved", { exact: true })).toHaveCount(0); + + await signOut(page); + await signIn(page, email, FIRST_PASSWORD); + await expect(page.getByRole("button", { name: "Account menu" })).toContainText("Wrong Current"); + + await signOut(page); + await signInAsAdmin(page); + await removeMember(page, email); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/native-mapper.spec.ts b/SW.Bitween.Web/ClientApp/e2e/native-mapper.spec.ts index 16f71486..4a8ae8a7 100644 --- a/SW.Bitween.Web/ClientApp/e2e/native-mapper.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/native-mapper.spec.ts @@ -14,12 +14,12 @@ import { * The new mapping editor, in a real browser against the real backend. * * The journeys: build a mapping, save, reload, and find exactly what was built. The - * old editor could not do that — it saved a generated Scriban template and + * old editor could not do that - it saved a generated Scriban template and * reverse-engineered the rules back out of it on load, so subtle detail came back * changed and nothing said so. * - * The mapping shapes themselves — every source, every transform, every kind of - * list — are tested below the browser: what the engine makes of them in C# + * The mapping shapes themselves - every source, every transform, every kind of + * list - are tested below the browser: what the engine makes of them in C# * (SW.Bitween.UnitTests/NativeMapper), and the editor's handling of them in * src/components/nativeMapper/__tests__. */ @@ -51,7 +51,7 @@ test("builds a mapping, previews it, saves it, and reloads exactly what was buil await page.getByRole("radio", { name: "Fixed" }).last().click(); await page.getByRole("textbox", { name: "Fixed value" }).last().fill("WEB"); - // A number target with a transform — the case that proves the arithmetic is done + // A number target with a transform - the case that proves the arithmetic is done // on the server with the real value rather than baked into a template. await addPathRule(page, "total", "order.net"); await openDetail(page, "total"); @@ -84,7 +84,7 @@ test("builds a mapping, previews it, saves it, and reloads exactly what was buil // 100 × 1.16. In binary floating point this is 115.99999999999999, which is why // the mapper works in decimal. It arrives as 116 rather than 116.00 because a - // whole number is written as an integer — an order quantity must not pick up a + // whole number is written as an integer - an order quantity must not pick up a // decimal point the source never had. await expect(preview).toContainText('"total": 116,'); @@ -153,7 +153,7 @@ test("choosing the new mapper offers its editor, and the old mapper keeps its ow // take the editor away from the one running subscriptions still use. // // Choosing an option leaves focus on the picker's input, and the list opens on a - // focus change — so picking twice from the same picker needs the focus released + // focus change - so picking twice from the same picker needs the focus released // first. A person never hits this: their next click lands long after focus has // settled, and the chevron toggles the list either way. await page.getByRole("combobox", { name: "mapper adapter" }).blur(); @@ -165,7 +165,7 @@ test("choosing the new mapper offers its editor, and the old mapper keeps its ow test("the editor opens for the mapper you picked, not the one that is saved", async ({ page }) => { // The link used to carry only the subscription, so the editor asked the server which // mapper it used and got the one being replaced. Choosing a mapper and opening its - // editor gave you the other one — in both directions — until you saved first, with + // editor gave you the other one - in both directions - until you saved first, with // nothing on screen to say why. const subscriptionId = await createSubscription(page); await writeMapperProperties(subscriptionId, "NativeJSONMapper", { ScribanTemplate: "{}" }); @@ -202,7 +202,7 @@ test("saving over the mapping the other mapper already has asks first", async ({ await addPathRule(page, "customer", "order.customer"); // Saving here switches the mapper as well as storing the rules, so the template - // someone wrote in the other editor goes — and nothing else holds a copy of it. + // someone wrote in the other editor goes - and nothing else holds a copy of it. await page.getByRole("button", { name: "Save" }).click(); await expect(page.getByText(/Replace the mapping this subscription already has/)).toBeVisible(); diff --git a/SW.Bitween.Web/ClientApp/e2e/notifiers.spec.ts b/SW.Bitween.Web/ClientApp/e2e/notifiers.spec.ts new file mode 100644 index 00000000..c2b692fc --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/notifiers.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from "@playwright/test"; +import { createServer, type IncomingMessage } from "node:http"; +import type { AddressInfo } from "node:net"; +import { AdminApi, stamp } from "./api"; +import { pickOption, signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * A notifier configured on its page, followed all the way to a notification arriving: the test + * listens on a local port, the notifier delivers there with the built-in HTTP handler, and an + * exchange that really fails is what sets it off. + */ + +/** A local HTTP endpoint that records every request made to it. */ +async function startInbox() { + const received: { method: string; url: string; body: string }[] = []; + const server = createServer((req: IncomingMessage, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + received.push({ method: req.method ?? "", url: req.url ?? "", body }); + res.statusCode = 200; + res.end("ok"); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/notify`; + return { url, received, close: () => new Promise((resolve) => server.close(() => resolve())) }; +} + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("a notifier set on its page to post failures of one subscription to an HTTP endpoint delivers when an exchange to it fails, and lists the delivery", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const inbox = await startInbox(); + const s = stamp(); + + // The seeded subscription whose every delivery fails: nothing listens where it sends. Used + // rather than one of the test's own so the failure it adds stays retryable for the exchanges + // spec - a failure whose subscription was deleted can't be retried. + const subscriptionName = SEED.failingSubscription; + const subscriptionId = await api.idOf("subscriptions", subscriptionName); + + try { + const notifierName = `Playwright Notifier ${s}`; + await page.goto("notifiers"); + await page.getByRole("button", { name: "New notifier" }).first().click(); + await page.fill("#nn-name", notifierName); + await page.getByRole("button", { name: "Create notifier" }).click(); + await expect(page).toHaveURL(/\/notifiers\/\d+$/); + const notifierId = Number(new URL(page.url()).pathname.split("/").pop()); + + await page.getByRole("checkbox", { name: /^An exchange fails/ }).check(); + await pickOption(page, "Deliver via", "NativeHttpHandler"); + await page.fill("#nf-prop-Url", inbox.url); + await page.getByRole("searchbox", { name: "Search subscriptions" }).fill(subscriptionName); + await page.getByRole("checkbox", { name: new RegExp(subscriptionName) }).check(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + + // Saved as set. + await page.reload(); + await expect(page.getByRole("checkbox", { name: /^An exchange fails/ })).toBeChecked(); + await expect(page.getByRole("checkbox", { name: /^An exchange succeeds/ })).not.toBeChecked(); + await expect(page.locator("#nf-prop-Url")).toHaveValue(inbox.url); + await page.getByRole("searchbox", { name: "Search subscriptions" }).fill(subscriptionName); + await expect(page.getByRole("checkbox", { name: new RegExp(subscriptionName) })).toBeChecked(); + + // An exchange to the watched subscription fails, and the notification arrives. + await api.sendExchange(subscriptionId, { shipment: `notify-${s}` }); + await expect + .poll(async () => (await api.exchangesOf(subscriptionId))[0]?.status ?? null, { + message: "the exchange never finished", + timeout: 60_000, + }) + .toBe(false); + const [{ id: exchangeId }] = await api.exchangesOf(subscriptionId); + await expect + .poll(() => inbox.received.length, { message: "no notification reached the endpoint", timeout: 60_000 }) + .toBeGreaterThan(0); + expect(inbox.received[0].method).toBe("POST"); + + // And the notifier's page records it against the exchange. + await expect(async () => { + await page.reload(); + const row = page.getByRole("row").filter({ hasText: exchangeId }); + await expect(row).toContainText("Sent", { timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + + await api.delete(`/notifiers/${notifierId}`); + } finally { + await inbox.close(); + } +}); + +test("a notifier switched off on its page sends nothing when a watched subscription fails", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const inbox = await startInbox(); + const s = stamp(); + + // The seeded subscription whose every delivery fails: nothing listens where it sends. Used + // rather than one of the test's own so the failure it adds stays retryable for the exchanges + // spec - a failure whose subscription was deleted can't be retried. + const subscriptionName = SEED.failingSubscription; + const subscriptionId = await api.idOf("subscriptions", subscriptionName); + + try { + await page.goto("notifiers"); + await page.getByRole("button", { name: "New notifier" }).first().click(); + await page.fill("#nn-name", `Playwright Quiet Notifier ${s}`); + await page.getByRole("button", { name: "Create notifier" }).click(); + await expect(page).toHaveURL(/\/notifiers\/\d+$/); + const notifierId = Number(new URL(page.url()).pathname.split("/").pop()); + + await page.getByRole("checkbox", { name: /^An exchange fails/ }).check(); + await pickOption(page, "Deliver via", "NativeHttpHandler"); + await page.fill("#nf-prop-Url", inbox.url); + await page.getByRole("searchbox", { name: "Search subscriptions" }).fill(subscriptionName); + await page.getByRole("checkbox", { name: new RegExp(subscriptionName) }).check(); + // The badge beside the name switches it off without losing its setup. + await page.getByRole("button", { name: "Active", exact: true }).click(); + await expect(page.getByRole("button", { name: "Off", exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + await page.reload(); + await expect(page.getByRole("button", { name: "Off", exact: true })).toBeVisible(); + + await api.sendExchange(subscriptionId, { shipment: `quiet-${s}` }); + await expect + .poll(async () => (await api.exchangesOf(subscriptionId))[0]?.status ?? null, { timeout: 60_000 }) + .toBe(false); + // Long enough for a notification that was going to be sent to have been. + await page.waitForTimeout(5000); + expect(inbox.received).toEqual([]); + + await api.delete(`/notifiers/${notifierId}`); + } finally { + await inbox.close(); + } +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts index 2519aea2..aa45386d 100644 --- a/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts @@ -1,5 +1,6 @@ import { test, expect, type Page } from "@playwright/test"; import { + API, FIRST_PASSWORD, addMember, createRole, @@ -8,6 +9,7 @@ import { signIn, signInAsAdmin, signOut, + sessionToken, } from "./helpers"; /** @@ -19,15 +21,15 @@ import { const sidebarLinks = (page: Page) => page.getByRole("navigation").getByRole("link").filter({ hasNotText: /^$/ }); -/** Calls the API directly with the signed-in member's own token — no UI in the way. */ +/** Calls the API directly with the signed-in member's own token - no UI in the way. */ async function apiStatus( page: Page, method: "GET" | "POST", path: string, body?: unknown, ): Promise { - const token = await page.evaluate(() => localStorage.getItem("access_token")); - const res = await page.request.fetch(`https://localhost:7155/api${path}`, { + const token = await sessionToken(page); + const res = await page.request.fetch(`${API}${path}`, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, ...(body === undefined ? {} : { data: body }), @@ -67,9 +69,9 @@ test("a custom role grants exactly what was ticked, in the nav, by URL, and at t await expect(page.getByRole("button", { name: /^Retry/ })).toHaveCount(0); // 4. And the API refuses the same things, so a crafted request gains nothing. - expect(await apiStatus(page, "POST", "/partners", { name: "sneaky" })).toBe(401); - expect(await apiStatus(page, "GET", "/accounts?limit=5")).toBe(401); - expect(await apiStatus(page, "POST", "/roles", { name: "x", description: "", permissions: [] })).toBe(401); + expect(await apiStatus(page, "POST", "/partners", { name: "sneaky" })).toBe(403); + expect(await apiStatus(page, "GET", "/accounts?limit=5")).toBe(403); + expect(await apiStatus(page, "POST", "/roles", { name: "x", description: "", permissions: [] })).toBe(403); await signOut(page); await signInAsAdmin(page); @@ -99,10 +101,10 @@ test("Viewer can read but not write", async ({ page }) => { await expect(page.getByText("You don't have access to this page")).toBeVisible(); // Writes are refused at the source, not just hidden. - expect(await apiStatus(page, "POST", "/partners", { name: "nope" })).toBe(401); - expect(await apiStatus(page, "POST", "/retrypolicies", { name: "nope" })).toBe(401); + expect(await apiStatus(page, "POST", "/partners", { name: "nope" })).toBe(403); + expect(await apiStatus(page, "POST", "/retrypolicies", { name: "nope" })).toBe(403); // Work groups had no guard of any kind until the handlers were given one. - expect(await apiStatus(page, "POST", "/workgroups", { name: "nope", busMessageName: "nope" })).toBe(401); + expect(await apiStatus(page, "POST", "/workgroups", { name: "nope", busMessageName: "nope" })).toBe(403); await signOut(page); await signInAsAdmin(page); diff --git a/SW.Bitween.Web/ClientApp/e2e/promoted-order.spec.ts b/SW.Bitween.Web/ClientApp/e2e/promoted-order.spec.ts index 04f2e005..597e5c8b 100644 --- a/SW.Bitween.Web/ClientApp/e2e/promoted-order.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/promoted-order.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { test("promoted properties can be put in order, and the order is kept", async ({ page }) => { const name = `Playwright Order ${Date.now()}`; await page.goto("information-types"); - await page.getByRole("button", { name: "New information type" }).click(); + await page.getByRole("button", { name: "New information type" }).first().click(); const dialog = page.getByRole("dialog", { name: "New information type" }); await dialog.getByRole("textbox", { name: "Name" }).fill(name); await dialog.getByRole("button", { name: "Create information type" }).click(); @@ -20,8 +20,8 @@ test("promoted properties can be put in order, and the order is kept", async ({ ["OrderNumber", "$.order"], ["City", "$.city"], ].entries()) { - await page.getByRole("button", { name: "Add friendly name" }).click(); - await page.getByRole("textbox", { name: `Friendly name ${i + 1}` }).fill(key); + await page.getByRole("button", { name: "Add promoted property" }).click(); + await page.getByRole("textbox", { name: `Name ${i + 1}`, exact: true }).fill(key); await page.getByRole("textbox", { name: `JSON path ${i + 1}` }).fill(path); } await page.getByRole("button", { name: "Save changes" }).click(); @@ -37,7 +37,7 @@ test("promoted properties can be put in order, and the order is kept", async ({ await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); await page.reload(); - const names = page.getByRole("textbox", { name: /^Friendly name \d/ }); + const names = page.getByRole("textbox", { name: /^Name \d+$/ }); await expect(names).toHaveCount(3); expect(await names.evaluateAll((boxes) => boxes.map((b) => (b as HTMLInputElement).value))).toEqual([ "OrderNumber", diff --git a/SW.Bitween.Web/ClientApp/e2e/queue-operations.spec.ts b/SW.Bitween.Web/ClientApp/e2e/queue-operations.spec.ts new file mode 100644 index 00000000..6a4e3307 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/queue-operations.spec.ts @@ -0,0 +1,141 @@ +import { test, expect, type APIRequestContext, type Page } from "@playwright/test"; +import { AdminApi, stamp } from "./api"; +import { E2E_RABBITMQ_MANAGEMENT as MQ } from "./env"; +import { signInAsAdmin } from "./helpers"; + +/** + * The two Queue health actions that change the broker: sending dead letters back, and deleting + * queues nothing reads. Their inputs - a message in a bad queue, a queue no consumer declares - + * only arise in production from a failure or a leftover, so the test puts them there through + * RabbitMQ's management API, and reads the broker's answer back the same way. + */ + +const mq = (request: APIRequestContext) => { + const headers = { + Authorization: `Basic ${Buffer.from(`${MQ.user}:${MQ.password}`).toString("base64")}`, + "Content-Type": "application/json", + }; + const queue = (name: string) => `${MQ.url}/api/queues/%2F/${encodeURIComponent(name)}`; + return { + reachable: async () => (await request.get(`${MQ.url}/api/overview`, { headers }).catch(() => null))?.ok() ?? false, + declare: async (name: string) => + expect((await request.put(queue(name), { headers, data: { durable: true } })).ok()).toBe(true), + publish: async (name: string, payload: string) => { + const res = await request.post(`${MQ.url}/api/exchanges/%2F/amq.default/publish`, { + headers, + data: { properties: {}, routing_key: name, payload, payload_encoding: "string" }, + }); + expect((await res.json()).routed, `nothing routed to ${name}`).toBe(true); + }, + /** Messages in the queue, or null once it is gone. */ + messages: async (name: string): Promise => { + const res = await request.get(queue(name), { headers }); + if (res.status() === 404) return null; + // From the broker's statistics, which lag by a few seconds: callers poll. + const q = (await res.json()) as { messages?: number; messages_ready?: number }; + return q.messages ?? q.messages_ready ?? 0; + }, + remove: (name: string) => request.delete(queue(name), { headers }), + }; +}; + +test.beforeEach(async ({ page, request }) => { + test.skip(!(await mq(request).reachable()), `RabbitMQ's management API is not reachable at ${MQ.url}`); + await signInAsAdmin(page); +}); + +/** This instance's queue prefix, as its own consumers are named - "v3.development.bitween", say. */ +async function queuePrefix(api: AdminApi) { + const consumers = await api.get<{ queueName: string }[]>("/ops/consumers"); + const sample = consumers.map((c) => c.queueName).find((q) => q.includes(".bitween.")); + expect(sample, "no consumer queue to read the prefix from").toBeTruthy(); + return sample!.slice(0, sample!.indexOf(".bitween.") + ".bitween".length); +} + +/** Reloads Queue health until `check` passes: its figures come from broker statistics that lag. */ +async function untilShown(page: Page, check: () => Promise) { + await expect(async () => { + await page.goto("queue-health"); + await expect(page.getByRole("heading", { name: "Queue health" })).toBeVisible(); + await check(); + }).toPass({ timeout: 60_000, intervals: [2000] }); +} + +test("a dead letter in a consumer's bad queue is listed on Queue health with its body, and Send back empties the bad queue", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const broker = mq(request); + // A consumer whose handling of this message is harmless: an unpause for a subscription that + // doesn't exist is acknowledged and does nothing, so the message sent back is consumed for good. + const consumers = await api.get<{ queueName: string }[]>("/ops/consumers"); + const main = consumers.map((c) => c.queueName).find((q) => q.endsWith(".subscriptionunpausedevent")); + expect(main, "no subscriptionunpausedevent consumer").toBeTruthy(); + const bad = `${main}.bad`; + const marker = `pw-dead-${stamp()}`; + + await broker.publish(bad, JSON.stringify({ Id: 0, Marker: marker })); + + // The count comes from broker statistics, which lag: it is only required to be there. The + // message itself is checked below, where the page reads the queue rather than its statistics. + const item = page.getByRole("listitem").filter({ has: page.getByText(bad, { exact: true }) }); + await untilShown(page, async () => { + await expect(item.getByText(/^\d+ dead$/)).toBeVisible({ timeout: 1000 }); + }); + + await item.getByRole("button", { name: "Show messages" }).click(); + const shown = page.getByRole("dialog", { name: /^Dead letters - / }); + await expect(shown.getByText(marker)).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(shown).toHaveCount(0); + + await item.getByRole("button", { name: "Send back" }).click(); + await page.getByRole("dialog", { name: /^Send (this message|\d+ messages) back\?$/ }).getByRole("button", { name: "Send back" }).click(); + await expect(item.getByRole("status")).toHaveText(/^(1 message|\d+ messages) sent back\.$/); + await expect.poll(() => broker.messages(bad), { message: "the bad queue still holds messages", timeout: 30_000 }).toBe(0); + // And they were taken off the main queue by its consumer, not left sitting there. + await expect.poll(() => broker.messages(main!), { timeout: 30_000 }).toBe(0); +}); + +test("a queue under this instance's prefix that nothing consumes is listed as unattended with what it holds, and deleting it there removes it and its bad queue from the broker", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const broker = mq(request); + const lane = `${await queuePrefix(api)}.xchangeservice.playwright-orphan-${stamp()}`; + + try { + await broker.declare(lane); + await broker.declare(`${lane}.bad`); + await broker.publish(lane, "{}"); + await broker.publish(lane, "{}"); + await broker.publish(`${lane}.bad`, "{}"); + + const row = page.getByRole("row").filter({ has: page.getByText(lane, { exact: true }) }); + await untilShown(page, async () => { + await expect(page.getByRole("heading", { name: "Nobody is reading these" })).toBeVisible({ timeout: 1000 }); + // Queued, retrying, dead. + await expect(row.getByRole("cell").nth(2)).toHaveText("2", { timeout: 1000 }); + await expect(row.getByRole("cell").nth(4)).toHaveText("1", { timeout: 1000 }); + }); + await expect(row).toContainText("+1"); + + await row.getByRole("checkbox", { name: `Select ${lane}` }).check(); + await expect(page.getByText("- holding 3 messages")).toBeVisible(); + await page.getByRole("button", { name: "Delete selected…" }).click(); + const confirm = page.getByRole("dialog", { name: "Delete these queues?" }); + await expect(confirm).toContainText("They still hold 3 messages"); + await confirm.getByRole("button", { name: "Delete queues" }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + + await expect.poll(() => broker.messages(lane), { message: "the queue is still on the broker", timeout: 30_000 }).toBeNull(); + expect(await broker.messages(`${lane}.bad`)).toBeNull(); + } finally { + await broker.remove(lane); + await broker.remove(`${lane}.bad`); + } +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/response-subscriptions.spec.ts b/SW.Bitween.Web/ClientApp/e2e/response-subscriptions.spec.ts new file mode 100644 index 00000000..751e2c19 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/response-subscriptions.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from "@playwright/test"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { AdminApi, stamp } from "./api"; +import { pickOption, signInAsAdmin, SUBSCRIPTION_PAGE } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * A response subscription built on its page and wired to a delivering subscription on that one's + * page, then followed through a real exchange: the delivery's answer is handed on, and the + * response subscription delivers it in turn. Both ends are HTTP endpoints this test runs. + */ + +/** A local endpoint that answers every request with `reply` and records what it was sent. */ +async function startEndpoint(reply: string) { + const received: string[] = []; + const server = createServer((req, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + received.push(body); + res.setHeader("Content-Type", "application/json"); + res.end(reply); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; + return { url, received, close: () => new Promise((resolve) => server.close(() => resolve())) }; +} + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("a response subscription made on its page and picked on a delivering subscription's Response step receives that delivery's answer and delivers it on", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const s = stamp(); + const answer = JSON.stringify({ label: `label-${s}` }); + const partnerSystem = await startEndpoint(answer); + const labelStore = await startEndpoint("{}"); + let feederId: number | null = null; + let responseId: number | null = null; + + try { + // The response subscription, on its own page. + const responseName = `Playwright Label Store ${s}`; + await page.goto("response-subscriptions"); + await page.getByRole("button", { name: "New response subscription" }).first().click(); + await page.fill("#nr-name", responseName); + await pickOption(page, "Information type", new RegExp(SEED.informationType)); + // Delivery is the step that is open to begin with. + await pickOption(page, "handler adapter", "NativeHttpHandler"); + await page.locator("#prop-Url").fill(labelStore.url); + await page.getByRole("button", { name: "Create response subscription" }).click(); + await expect(page).toHaveURL(SUBSCRIPTION_PAGE); + responseId = Number(new URL(page.url()).pathname.split("/").pop()); + + // A subscription whose delivery answers with a document. + const feederName = `Playwright Label Request ${s}`; + feederId = await api.post("/subscriptions", { + name: feederName, + documentId: await api.idOf("documents", SEED.informationType), + type: "ApiCall", + partnerId: await api.idOf("partners", SEED.partner), + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: partnerSystem.url }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + inactive: false, + }); + + // Hand its answer to the response subscription, on its Response step. + await page.goto(`subscriptions/${feederId}`); + await page.getByRole("button", { name: /^Response/ }).click(); + await pickOption(page, "Hand the response to", responseName); + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + await page.reload(); + await expect(page.getByRole("combobox", { name: "Hand the response to" })).toHaveValue(responseName); + expect((await api.get<{ responseSubscriptionId: number | null }>(`/subscriptions/${feederId}`)).responseSubscriptionId).toBe( + responseId, + ); + + // The list of response subscriptions names what feeds this one. + await page.goto("response-subscriptions"); + await expect(page.getByRole("row").filter({ hasText: responseName })).toContainText(feederName); + + // A document goes out, the partner system answers, and the answer is delivered on. + await api.sendExchange(feederId, { shipment: `label-me-${s}` }); + await expect + .poll(() => partnerSystem.received.length, { message: "the delivery never reached the partner system", timeout: 60_000 }) + .toBe(1); + expect(partnerSystem.received[0]).toContain(`label-me-${s}`); + await expect + .poll(() => labelStore.received.length, { message: "the response subscription never delivered", timeout: 60_000 }) + .toBe(1); + expect(JSON.parse(labelStore.received[0])).toEqual(JSON.parse(answer)); + // And it ran as an exchange of its own, which succeeded. + await expect + .poll(async () => (await api.exchangesOf(responseId!))[0]?.status ?? null, { timeout: 30_000 }) + .toBe(true); + } finally { + await partnerSystem.close(); + await labelStore.close(); + if (feederId !== null) await api.delete(`/subscriptions/${feederId}`); + if (responseId !== null) await api.delete(`/subscriptions/${responseId}`); + } +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/retention.spec.ts b/SW.Bitween.Web/ClientApp/e2e/retention.spec.ts index 063831a9..2bf2382d 100644 --- a/SW.Bitween.Web/ClientApp/e2e/retention.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/retention.spec.ts @@ -1,5 +1,5 @@ import { test, expect, type Page } from "@playwright/test"; -import { signInAsAdmin } from "./helpers"; +import { sessionToken, signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { await signInAsAdmin(page); @@ -7,21 +7,44 @@ test.beforeEach(async ({ page }) => { /** The backend's own API, called with the signed-in admin's token. */ async function api(page: Page, path: string): Promise { - return page.evaluate(async (p) => { - const token = localStorage.getItem("access_token"); - const res = await fetch(`/api${p}`, { headers: { Authorization: `Bearer ${token}` } }); - return res.json(); - }, path); + const token = await sessionToken(page); + return page.evaluate( + async ({ p, t }) => { + const res = await fetch(`/api${p}`, { headers: { Authorization: `Bearer ${t}` } }); + return res.json(); + }, + { p: path, t: token }, + ); } -test("the storage section explains retention, previews a change and asks before saving it", async ({ page }) => { - await page.goto("settings?section=Documents%20%26%20storage"); +/** + * Whether the bucket deletes exchange files by age at all. The cloud providers' buckets carry a + * temp30/ rule; local-disk storage - what tools/e2e.sh runs on - reports no rules, because + * nothing on a local filesystem deletes files by age. The tests about that rule can only run + * where it exists. + */ +async function hasTemp30Rule(page: Page) { + const status = await api<{ storage: { rules: { prefix: string; enabled: boolean }[] } }>(page, "/retention"); + return status.storage.rules.some((r) => r.prefix === "temp30/" && r.enabled); +} - await expect(page.getByText("What these settings do")).toBeVisible(); - // The bucket's real rules, with the one new exchange files fall under marked. +const NO_RULE = "this storage bucket has no temp30/ deletion rule (local-disk storage never has one)"; + +test("the storage section lists the bucket's rules and marks the one exchange files fall under", async ({ + page, +}) => { + test.skip(!(await hasTemp30Rule(page)), NO_RULE); + + await page.goto("settings?section=Documents%20%26%20storage"); const rules = page.getByRole("table"); await expect(rules.getByText("temp30/", { exact: true })).toBeVisible(); await expect(rules.getByText("Exchange files")).toBeVisible(); +}); + +test("the storage section explains retention, previews a change and asks before saving it", async ({ page }) => { + await page.goto("settings?section=Documents%20%26%20storage"); + + await expect(page.getByText("What these settings do")).toBeVisible(); const days = page.getByRole("spinbutton", { name: "Keep exchanges (days)" }); await days.fill("45"); @@ -42,7 +65,9 @@ test("the storage section explains retention, previews a change and asks before }); test("an exchange whose file the bucket deleted says so in its drawer", async ({ page }) => { - // Anything older than the temp30/ rule has had its files deleted by the local bucket. + test.skip(!(await hasTemp30Rule(page)), NO_RULE); + + // Anything older than the temp30/ rule has had its files deleted by the bucket. const before = new Date(Date.now() - 35 * 24 * 3600 * 1000).toISOString(); const found = await api<{ result: { id: string }[] }>( page, diff --git a/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts b/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts index 76bcf1d9..bb66c377 100644 --- a/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/retry-policies.spec.ts @@ -1,14 +1,8 @@ import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("retry policy create, add group with fixed delay, dry-run, list, delete", async ({ page }) => { @@ -16,7 +10,7 @@ test("retry policy create, add group with fixed delay, dry-run, list, delete", a const groupName = "Timeouts"; await page.goto("retry-policies"); - await page.getByRole("button", { name: "New retry policy" }).click(); + await page.getByRole("button", { name: "New retry policy" }).first().click(); await page.fill("#nrp-name", name); await page.getByRole("button", { name: "Create policy" }).click(); @@ -29,7 +23,7 @@ test("retry policy create, add group with fixed delay, dry-run, list, delete", a await dialog.locator("#rg-name").fill(groupName); await dialog.getByRole("button", { name: "Add condition" }).click(); await dialog.getByLabel("Text to find").fill("timeout"); - // Fixed delay in seconds — the backend stores this in milliseconds, so this + // Fixed delay in seconds - the backend stores this in milliseconds, so this // exercises the ms <-> seconds conversion in both directions once reloaded. await dialog.locator("#rg-delay").selectOption("fixed"); await dialog.locator("#rg-d1").fill("45"); @@ -39,7 +33,7 @@ test("retry policy create, add group with fixed delay, dry-run, list, delete", a await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); - // Reload forces a fresh GET — proves the group (incl. the delay unit + // Reload forces a fresh GET - proves the group (incl. the delay unit // conversion) actually round-tripped through the backend correctly. await page.reload(); await expect(page.getByText(groupName)).toBeVisible(); @@ -51,7 +45,8 @@ test("retry policy create, add group with fixed delay, dry-run, list, delete", a // Dry-run against the saved (now unsaved-clean) groups. await page.fill("#tp-content", "System.Net.Http.HttpRequestException: timeout while connecting"); await page.getByRole("button", { name: "Run simulation" }).click(); - const attempt = page.locator("ol li").first(); + // Not the breadcrumb's list: the attempts list on the page. + const attempt = page.locator(":not(nav) > ol li").first(); await expect(attempt).toContainText("Retries"); await expect(attempt).toContainText("Next try in 45s"); diff --git a/SW.Bitween.Web/ClientApp/e2e/scheduled-runs.spec.ts b/SW.Bitween.Web/ClientApp/e2e/scheduled-runs.spec.ts new file mode 100644 index 00000000..3e205409 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/scheduled-runs.spec.ts @@ -0,0 +1,180 @@ +import { test, expect, type Page } from "@playwright/test"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { AdminApi, stamp } from "./api"; +import { pickOption, signInAsAdmin, SUBSCRIPTION_PAGE } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * Running a schedule now instead of waiting for it: Receive now on a scheduled job, and Roll up + * now on an aggregation. Each is followed to what it should cause - a document fetched from a + * source and delivered, a roll-up of the source's exchanges delivered - at endpoints this test + * runs, and to the run it records on the subscription's page. + */ + +/** A local endpoint that answers every request with `reply` and records what it was sent. */ +async function startEndpoint(reply = "ok") { + const received: { method: string; body: string }[] = []; + const server = createServer((req, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + received.push({ method: req.method ?? "", body }); + res.setHeader("Content-Type", "application/json"); + res.end(reply); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; + return { url, received, close: () => new Promise((resolve) => server.close(() => resolve())) }; +} + +/** A schedule twelve hours from now, so it never fires while the test is running. */ +const farSchedule = () => ({ + recurrence: "Daily", + days: 0, + hours: (new Date().getUTCHours() + 12) % 24, + minutes: 0, + backwards: false, +}); + +/** Presses a row's run-now button on a list page and confirms. */ +async function runNow(page: Page, list: string, name: string, button: string, dialog: string) { + await page.goto(list); + const row = page.getByRole("row").filter({ hasText: name }); + await row.getByRole("button", { name: button }).click(); + await page.getByRole("dialog", { name: dialog }).getByRole("button", { name: button }).click(); + await expect(page.getByRole("dialog")).toHaveCount(0); +} + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("Receive now on the scheduled jobs list fetches the job's source once and delivers what it found, and the job's page records the run", async ({ + page, + request, +}) => { + test.setTimeout(120_000); + const api = await AdminApi.signIn(request); + const s = stamp(); + const document = JSON.stringify({ shipment: `fetched-${s}` }); + const source = await startEndpoint(document); + const sink = await startEndpoint(); + const name = `Playwright Fetch Job ${s}`; + let id: number | null = null; + + try { + id = await api.post("/subscriptions", { + name, + documentId: await api.idOf("documents", SEED.informationType), + type: "Receiving", + receiverId: "NativeHttpReceiver", + receiverProperties: [{ key: "Url", value: source.url }], + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: sink.url }], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + schedules: [farSchedule()], + inactive: false, + }); + + await runNow(page, "scheduled-jobs", name, "Receive now", "Receive now?"); + + await expect.poll(() => source.received.length, { message: "the source was never fetched", timeout: 60_000 }).toBe(1); + await expect.poll(() => sink.received.length, { message: "what was fetched never arrived", timeout: 60_000 }).toBe(1); + expect(JSON.parse(sink.received[0].body)).toEqual(JSON.parse(document)); + + await expect(async () => { + await page.goto(`subscriptions/${id}`); + await expect(page.getByText("Received 1 item")).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + } finally { + await source.close(); + await sink.close(); + if (id !== null) await api.delete(`/subscriptions/${id}`); + } +}); + +test("an aggregation made on its page and rolled up now delivers one roll-up naming each successful exchange of its source, and a second roll-up with nothing new delivers nothing", async ({ + page, + request, +}) => { + test.setTimeout(150_000); + const api = await AdminApi.signIn(request); + const s = stamp(); + const partnerSystem = await startEndpoint(); + const manifestStore = await startEndpoint(); + const sourceName = `Playwright Agg Source ${s}`; + const name = `Playwright Daily Manifest ${s}`; + let sourceId: number | null = null; + let aggregationId: number | null = null; + + try { + sourceId = await api.post("/subscriptions", { + name: sourceName, + documentId: await api.idOf("documents", SEED.informationType), + type: "ApiCall", + partnerId: await api.idOf("partners", SEED.partner), + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: partnerSystem.url }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + inactive: false, + }); + await api.sendExchange(sourceId, { shipment: `one-${s}` }); + await api.sendExchange(sourceId, { shipment: `two-${s}` }); + let collected: string[] = []; + await expect + .poll(async () => { + collected = (await api.exchangesOf(sourceId!)).filter((x) => x.status === true).map((x) => x.id); + return collected.length; + }, { timeout: 60_000 }) + .toBe(2); + + await page.goto("aggregations/new"); + await page.fill("#na-name", name); + await pickOption(page, "Subscription to roll up", sourceName); + await page.getByRole("button", { name: /^Schedule/ }).click(); + await page.getByRole("button", { name: "Add schedule" }).click(); + const schedule = farSchedule(); + await page.locator("#sc-rec").selectOption("Daily"); + await page.fill("#sc-h", String(schedule.hours)); + await page.fill("#sc-m", "0"); + await page.getByRole("dialog", { name: "Add schedule" }).getByRole("button", { name: "Add schedule" }).click(); + await page.getByRole("button", { name: /^Delivery/ }).click(); + await pickOption(page, "handler adapter", "NativeHttpHandler"); + await page.locator("#prop-Url").fill(manifestStore.url); + await page.getByRole("checkbox", { name: /^Enable immediately/ }).check(); + await page.getByRole("button", { name: "Create aggregation" }).click(); + await expect(page).toHaveURL(SUBSCRIPTION_PAGE); + aggregationId = Number(new URL(page.url()).pathname.split("/").pop()); + + await runNow(page, "aggregations", name, "Roll up now", "Roll up now?"); + await expect + .poll(() => manifestStore.received.length, { message: "the roll-up was never delivered", timeout: 60_000 }) + .toBe(1); + for (const id of collected) expect(manifestStore.received[0].body).toContain(id); + + await expect(async () => { + await page.goto(`subscriptions/${aggregationId}`); + await expect(page.locator("td").getByText("Rolled up", { exact: true })).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + + // Everything outstanding was collected, so the next run has nothing to do. + await runNow(page, "aggregations", name, "Roll up now", "Roll up now?"); + await expect(async () => { + await page.goto(`subscriptions/${aggregationId}`); + await expect(page.locator("td").getByText("Nothing to roll up", { exact: true })).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + expect(manifestStore.received).toHaveLength(1); + } finally { + await partnerSystem.close(); + await manifestStore.close(); + if (aggregationId !== null) await api.delete(`/subscriptions/${aggregationId}`); + if (sourceId !== null) await api.delete(`/subscriptions/${sourceId}`); + } +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/seed-data.ts b/SW.Bitween.Web/ClientApp/e2e/seed-data.ts new file mode 100644 index 00000000..7a318f0b --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/seed-data.ts @@ -0,0 +1,47 @@ +/** + * The rows the suite depends on, by name. + * + * The `seed` setup project finds or creates each of these before any spec runs, so the suite + * works on a database created a minute ago as well as on a long-lived dev database that already + * has them. Specs refer to these constants rather than repeating the strings, so a rename here + * cannot leave a spec looking for a row that is no longer made. + * + * None of them may match the purge patterns in seed.setup.ts ("Playwright …", "PW …"): those + * rows are deleted at the start of every run, and a seeded row caught by them would be deleted + * and recreated each time - or, worse, deleted after something had been built on top of it. + */ +export const SEED = { + /** A partner to attach to gateways. Has no adapters or keys of its own. */ + partner: "Acme Retail", + + /** A JSON information type: what every subscription the specs create is about. */ + informationType: "Shipment order", + /** + * Its code. Optional in the product, but real types carry one, and list pages show the code + * where there is one - table-layout.spec relies on that column holding a short code. + */ + informationTypeCode: "SHIPMENT_ORDER", + + /** + * A bus-enabled information type that no bus gateway listens for. Creating a bus gateway + * needs one, and a type can only have one gateway, so the seed refuses to go on if something + * other than the suite's own leftovers has claimed it. + */ + busInformationType: "Delivery proof", + busInformationTypeCode: "DELIVERY_PROOF", + /** Its bus message name. No spaces: it becomes a RabbitMQ routing key. */ + busMessageTypeName: "e2e.delivery-proof", + /** Its one promoted property. A route's filter can only match on promoted properties. */ + busFilterProperty: { key: "Carrier", path: "$.carrier" }, + + /** + * A subscription that delivers to a port nothing listens on, so every exchange addressed to it + * fails. The exchanges, dashboard and table-layout specs need failed exchanges to act on, and + * this is the one honest way to get them: a real delivery that really failed. + */ + failingSubscription: "E2E unreachable delivery", + failingUrl: "http://127.0.0.1:9/unreachable", + + /** How many failed exchanges nobody has retried yet the specs can count on. */ + unretriedFailures: 3, +} as const; diff --git a/SW.Bitween.Web/ClientApp/e2e/seed.setup.ts b/SW.Bitween.Web/ClientApp/e2e/seed.setup.ts new file mode 100644 index 00000000..b3551a0b --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/seed.setup.ts @@ -0,0 +1,431 @@ +import { expect, request, test as setup, type APIRequestContext } from "@playwright/test"; +import { ADMIN_EMAIL, ADMIN_PASSWORD, API, BASE_URL } from "./env"; +import { SEED } from "./seed-data"; + +/** + * Puts the database into the state the suite expects, before any spec runs. + * + * Two jobs, in this order. First it removes what earlier runs left behind: a test that fails + * part-way skips its own cleanup, and the leftovers aren't harmless - a stray account still + * holding Administrator is enough to make the last-administrator guard pass, which strips the + * seeded admin's role and fails everything after it. Then it makes sure the rows the specs build + * on exist (see seed-data.ts), finding each by name and creating only what is missing, so it is + * as happy on a database created a minute ago as on a team database that has had them for months. + * + * It is a setup project rather than a globalSetup so that a failure here is reported like any + * other test - with a trace and a step that says which prerequisite is missing - rather than as + * a stack trace before the run starts. Everything goes through the same HTTP API the UI uses. + */ + +const ADMINISTRATOR_ROLE_ID = 1; + +const TEST_EMAIL = /^pw-.*@example\.test$/; +const TEST_ROLE = /^PW /; +/** Everything the suite creates is named this way, so it can be found again and removed. */ +const TEST_NAME = /^Playwright /; +/** The only settings the suite writes to - see the reset below for why this is a list, not "all". */ +const TEST_SETTINGS = ["Theme.PrimaryColor", "Theme.TabTitle", "Theme.CompanyName"]; + +interface Account { + id: number; + email: string; + roles: { id: number; name: string }[] | null; +} +interface Named { + id: number; + name: string; +} +interface RawDocument { + code: string | null; + name: string; + documentFormat: string; + busEnabled: boolean; + busMessageTypeName: string | null; + duplicateInterval: number; + disregardsUnfilteredMessages: boolean; + promotedProperties: { key: string; value: string }[] | null; +} + +// One serial chain: if the backend can't be reached there is no point purging, and if the purge +// fails the seed would be built on top of whatever it left. +setup.describe.configure({ mode: "serial" }); + +let api: APIRequestContext; +let token = ""; +const auth = () => ({ Authorization: `Bearer ${token}` }); + +async function signIn() { + const login = await api.post(`${API}/accounts/login`, { + data: { Username: ADMIN_EMAIL, Password: ADMIN_PASSWORD }, + }); + if (!login.ok()) + throw new Error( + `Can't sign in as ${ADMIN_EMAIL} (${login.status()}: ${await login.text()}). ` + + "Set E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD to the administrator of the instance under test. " + + "A fresh install takes its password from Bitween__InitialAdminPassword, which tools/e2e.sh sets " + + "to E2E_ADMIN_PASSWORD; an older dev database still has the published default.", + ); + token = (await login.json()).jwt; +} + +/** A JSON body, or a failure that says which call it was and what the server answered. */ +async function ok(what: string, res: Awaited>): Promise { + if (!res.ok()) throw new Error(`${what} failed: ${res.status()} ${await res.text()}`); + const text = await res.text(); + return (text ? JSON.parse(text) : null) as T; +} + +/** + * The row with exactly this name, or undefined. Searched by name rather than listed whole, so a + * long-lived database with thousands of rows can't push the one wanted off the page. + */ +async function findByName(list: string, name: string): Promise { + const filter = encodeURIComponent(`Name:4:${name}`); + const res = await ok<{ result: Named[] }>( + `searching ${list} for "${name}"`, + await api.get(`${API}/${list}?filter=${filter}&page=0&size=100`, { headers: auth() }), + ); + return (res.result ?? []).find((r) => r.name === name); +} + +/** + * The information type with this name, created if missing. One that exists already - on a team + * database, say - keeps everything it has; only what the specs rely on and it lacks is added: + * a code where it has none, and any of the wanted promoted properties. The update replaces the + * whole row, so it is sent back with every field as read. + */ +async function ensureDocument( + name: string, + want: { + code: string; + busEnabled: boolean; + busMessageTypeName?: string; + promotedProperties: { key: string; value: string }[]; + }, +): Promise { + const found = await findByName("documents", name); + if (!found) + return ok( + `creating information type "${name}"`, + await api.post(`${API}/documents`, { + headers: auth(), + data: { + name, + documentFormat: "Json", + duplicateInterval: 0, + disregardsUnfilteredMessages: false, + validationSchema: "", + ...want, + }, + }), + ); + + const doc = await ok( + `reading information type "${name}"`, + await api.get(`${API}/documents/${found.id}`, { headers: auth() }), + ); + expect(doc.documentFormat, `information type "${name}" must be JSON`).toBe("Json"); + if (want.busEnabled) expect(doc.busEnabled, `information type "${name}" must be bus-enabled`).toBe(true); + + const have = doc.promotedProperties ?? []; + const missing = want.promotedProperties.filter((w) => !have.some((h) => h.value === w.value)); + if (doc.code && missing.length === 0) return found.id; + + await ok( + `completing information type "${name}"`, + await api.post(`${API}/documents/${found.id}`, { + headers: auth(), + data: { + id: found.id, + code: doc.code || want.code, + name: doc.name, + documentFormat: doc.documentFormat, + busEnabled: doc.busEnabled, + busMessageTypeName: doc.busMessageTypeName ?? undefined, + duplicateInterval: doc.duplicateInterval, + disregardsUnfilteredMessages: doc.disregardsUnfilteredMessages, + promotedProperties: [...have, ...missing], + }, + }), + ); + return found.id; +} + +setup.beforeAll(async () => { + api = await request.newContext({ ignoreHTTPSErrors: true }); +}); + +setup.afterAll(async () => { + await api?.dispose(); +}); + +setup("the backend is up and the administrator can sign in", async () => { + const health = await api.get(`${BASE_URL}health`).catch((e: Error) => e); + if (health instanceof Error || !health.ok()) + throw new Error( + `Nothing is answering at ${BASE_URL}health. Start Bitween there first - tools/e2e.sh starts a ` + + "throwaway instance with everything it needs - or point E2E_BASE_URL at one that is running.", + ); + + await signIn(); + + // The seeded admin used to ship with a published password, so a database migrated back then + // flags it and issues it a token that grants nothing until someone chooses a new one. Setting + // it back to the same value clears the flag, which is the whole of what the suite needs: every + // spec signs in as this account, and none of them is about the forced change itself. On a fresh + // install the password came from Bitween__InitialAdminPassword and was never flagged, and this + // changes nothing. + // + // Deliberately loud about what it is. Re-setting a known password is exactly the thing the flag + // exists to prevent - it is defensible here only because this database exists to be tested on. + await api.post(`${API}/accounts/changePassword`, { + headers: auth(), + data: { OldPassword: ADMIN_PASSWORD, NewPassword: ADMIN_PASSWORD }, + }); +}); + +setup("leftovers from earlier runs are removed", async () => { + const accounts = await api.get(`${API}/accounts?limit=500`, { headers: auth() }); + + if (!accounts.ok()) { + // There used to be a fallback here: a second sign-in against credentials held in + // configuration, which worked even for an admin holding no roles. That endpoint shipped with a + // published default password and has been removed, so this is now the only way in - and if the + // seeded admin has lost Administrator, no request can put it back. + throw new Error( + `Signed in as ${ADMIN_EMAIL} but can't list accounts (${accounts.status()}). The admin has ` + + "probably lost its Administrator role, and the database needs restoring.", + ); + } + + const rows: Account[] = (await accounts.json()).result ?? []; + + // The seeded admin must hold Administrator, or nothing downstream can manage anything. + const admin = rows.find((a) => a.email.toLowerCase() === ADMIN_EMAIL.toLowerCase()); + if (admin && !(admin.roles ?? []).some((r) => r.id === ADMINISTRATOR_ROLE_ID)) { + await api.post(`${API}/accounts/${admin.id}/setRoles`, { + headers: auth(), + data: { roleIds: [ADMINISTRATOR_ROLE_ID] }, + }); + // Re-read as the repaired admin so the purge below runs with full permissions. + await signIn(); + } + + for (const account of rows.filter((a) => TEST_EMAIL.test(a.email))) + await api.post(`${API}/accounts/${account.id}/remove`, { headers: auth(), data: {} }); + + const roles = await api.get(`${API}/roles?pageSize=500`, { headers: auth() }); + for (const role of ((await roles.json()).result ?? []) as Named[]) + if (TEST_ROLE.test(role.name)) await api.delete(`${API}/roles/${role.id}`, { headers: auth() }); + + // Settings tests assert against product defaults, so a value left behind by a failed run would + // make them fail for the wrong reason. Only the keys the tests actually touch are reset: the + // Settings table is now the only home for values like the MSAL ids and the Rebex license key - + // configuration is read once at first boot and ignored after that - so a blanket reset here + // would destroy real configuration with no way to get it back. + for (const key of TEST_SETTINGS) + await api.delete(`${API}/settings/${encodeURIComponent(key)}`, { headers: auth() }); + + // The domain objects the suite creates outlive a failed run too, and they are not harmless + // either: enough of them push a newly created row off the first page of a list, and the tests + // that look for their own row then fail for a reason that has nothing to do with them. + // + // Order matters. A gateway holds attachments and routes that reference subscriptions, so the + // gateways go first or the subscriptions underneath them refuse to delete. + const purge = async (list: string, remove: (id: number) => Promise) => { + const res = await api.get(`${API}/${list}?size=500&limit=500`, { headers: auth() }); + if (!res.ok()) return; + const rows = ((await res.json()).result ?? []) as Named[]; + for (const row of rows.filter((r) => TEST_NAME.test(r.name ?? ""))) { + // Best effort: something still referencing a row is not a reason to abandon the rest. + try { + await remove(row.id); + } catch { + /* leave it for the next run */ + } + } + }; + + await purge("apigateways", (id) => api.delete(`${API}/apigateways/${id}`, { headers: auth() })); + await purge("busgateways", (id) => api.delete(`${API}/busgateways/${id}`, { headers: auth() })); + // A notifier watches subscriptions, and a subscription reads from a data source: each goes + // before what it points at. + await purge("notifiers", (id) => api.delete(`${API}/notifiers/${id}`, { headers: auth() })); + await purge("subscriptions", (id) => api.delete(`${API}/subscriptions/${id}`, { headers: auth() })); + await purge("datasources", (id) => api.delete(`${API}/datasources/${id}`, { headers: auth() })); + await purge("workgroups", (id) => + api.post(`${API}/workgroups/${id}/delete`, { headers: auth(), data: {} }), + ); + await purge("documents", (id) => api.delete(`${API}/documents/${id}`, { headers: auth() })); + await purge("partners", (id) => api.delete(`${API}/partners/${id}`, { headers: auth() })); + + // Values sets are keyed by a string id rather than a number, so `purge` cannot do them. + const sets = await api.get(`${API}/globaladaptervaluessets?size=500&limit=500`, { headers: auth() }); + if (sets.ok()) + for (const set of ((await sets.json()).result ?? []) as { id: string; name: string }[]) + if (TEST_NAME.test(set.name ?? "")) + await api.post(`${API}/globaladaptervaluessets/${set.id}/delete`, { + headers: auth(), + data: {}, + }); +}); + +setup("the pieces the specs need are installed", async () => { + setup.setTimeout(90_000); + + // Exchanges are processed by the app's own RabbitMQ consumers. Without them every exchange the + // seed creates stays "processing" forever, and the wait below would time out saying only that. + // + // Polled rather than read once: an app that has only just started answers before its consumers + // are attached, and the broker's management API, which this reads through, lags behind it. + const attached = async () => { + const consumers = await ok<{ name: string; totalNodes: number }[]>( + "reading the RabbitMQ consumers (/api/ops/consumers)", + await api.get(`${API}/ops/consumers`, { headers: auth() }), + ); + return consumers.filter((c) => c.totalNodes > 0).length; + }; + await expect + .poll(attached, { + message: + "No RabbitMQ consumer is attached to Bitween's queues, so no exchange would ever be processed. " + + "Is the broker in ConnectionStrings__RabbitMQ reachable, and Bitween__RabbitMqManagementUrl set?", + timeout: 60_000, + }) + .toBeGreaterThan(0); + + // The specs build every subscription out of the two in-process HTTP adapters. + for (const [kind, key] of [ + ["handlers", "NativeHttpHandler"], + ["receivers", "NativeHttpReceiver"], + ]) { + const catalog = await ok<{ key: string }[]>( + `reading the ${kind} catalog`, + await api.get(`${API}/adapters/Catalog?prefix=${kind}`, { headers: auth() }), + ); + expect( + catalog.map((a) => a.key), + `${key} is missing from /api/adapters/Catalog?prefix=${kind}`, + ).toContain(key); + } +}); + +setup("the seeded rows exist", async () => { + setup.setTimeout(120_000); + + const partnerId = + (await findByName("partners", SEED.partner))?.id ?? + (await ok( + `creating partner "${SEED.partner}"`, + await api.post(`${API}/partners`, { + headers: auth(), + data: { name: SEED.partner, adapterProperties: {}, secretProperties: [], loginIdentity: null }, + }), + )); + + const filterProperty = { key: SEED.busFilterProperty.key, value: SEED.busFilterProperty.path }; + const shipmentOrderId = await ensureDocument(SEED.informationType, { + code: SEED.informationTypeCode, + busEnabled: false, + promotedProperties: [], + }); + const deliveryProofId = await ensureDocument(SEED.busInformationType, { + code: SEED.busInformationTypeCode, + busEnabled: true, + busMessageTypeName: SEED.busMessageTypeName, + promotedProperties: [filterProperty], + }); + + // The bus gateway spec creates the gateway for this type, and a type can only have one. The + // suite's own leftovers were purged above, so anything still here belongs to somebody else. + const gateways = await ok<{ result: (Named & { documentId: number })[] }>( + "listing bus gateways", + await api.get(`${API}/busgateways?page=0&size=500`, { headers: auth() }), + ); + const claimed = (gateways.result ?? []).find((g) => g.documentId === deliveryProofId); + expect( + claimed, + `bus gateway "${claimed?.name}" already listens for "${SEED.busInformationType}", which the ` + + "bus gateway spec needs free. Delete that gateway, or move it to another information type.", + ).toBeUndefined(); + + // The failing subscription. Created live (inactive: false) because exchanges addressed to an + // inactive subscription are never delivered, and without a retry policy so a failure stays a + // failure instead of turning into a scheduled retry. + const existing = await findByName("subscriptions", SEED.failingSubscription); + const subscriptionId = + existing?.id ?? + (await ok( + `creating subscription "${SEED.failingSubscription}"`, + await api.post(`${API}/subscriptions`, { + headers: auth(), + data: { + name: SEED.failingSubscription, + documentId: shipmentOrderId, + type: "ApiCall", + partnerId, + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: SEED.failingUrl }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + retryPolicyId: null, + customRetryPolicy: null, + runOnBadResponses: false, + inactive: false, + }, + }), + )); + + // On a long-lived database someone may have switched it off or paused it; either stops the + // exchanges below from ever being delivered. + if (existing) { + const raw = await ok<{ inactive: boolean; pausedOn: string | null }>( + "reading the failing subscription", + await api.get(`${API}/subscriptions/${subscriptionId}`, { headers: auth() }), + ); + expect( + raw.inactive || raw.pausedOn !== null, + `subscription "${SEED.failingSubscription}" is disabled or paused; enable and resume it`, + ).toBe(false); + } + + // Every exchange it gets fails, so failures that nobody has retried are a matter of sending + // enough of them. The specs retry some on every run, and each retry fails in turn and leaves a + // new un-retried attempt at the end of its chain - so after the first run this usually sends + // nothing at all. + const unretried = async () => { + const filter = ["StatusFilter:1:3", "LatestOnly:1:true", `SubscriptionId:1:${subscriptionId}`] + .map((f) => `filter=${encodeURIComponent(f)}`) + .join("&"); + const res = await ok<{ totalCount: number }>( + "counting failed exchanges", + await api.get(`${API}/xchanges?${filter}&page=0&size=1`, { headers: auth() }), + ); + return res.totalCount; + }; + + const missing = SEED.unretriedFailures - (await unretried()); + for (let i = 0; i < missing; i++) + await ok( + "creating an exchange", + await api.post(`${API}/xchanges`, { + headers: auth(), + data: { + option: "SubscriberId", + subscriberId: subscriptionId, + data: JSON.stringify({ shipment: `e2e-seed-${Date.now()}-${i}` }), + }, + }), + ); + + await expect + .poll(unretried, { + message: + `the exchanges sent to "${SEED.failingSubscription}" were never processed - are Bitween's ` + + "RabbitMQ consumers running?", + timeout: 90_000, + }) + .toBeGreaterThanOrEqual(SEED.unretriedFailures); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/service-accounts.spec.ts b/SW.Bitween.Web/ClientApp/e2e/service-accounts.spec.ts new file mode 100644 index 00000000..b035f494 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/service-accounts.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from "@playwright/test"; +import { API, signInAsAdmin } from "./helpers"; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("a service account's token is shown once, signs in, and stops when revoked", async ({ page, request }) => { + const name = `playwright-svc-${Date.now()}`; + + await page.goto("team/service-accounts"); + await page.getByRole("button", { name: "New service account" }).first().click(); + await page.getByLabel("Name").fill(name); + await page.getByRole("checkbox", { name: /^Viewer/ }).check(); + await page.getByRole("button", { name: "Create" }).click(); + + const drawer = page.getByRole("dialog", { name: "Service account details" }); + await expect(drawer.getByRole("heading", { name })).toBeVisible(); + + await drawer.getByLabel("New token").fill("e2e job"); + await drawer.getByRole("button", { name: "Issue token" }).click(); + const token = (await drawer.locator("code").first().textContent())!.trim(); + expect(token).toMatch(/^bwt_[A-Za-z0-9]{40}$/); + + // The token signs in, as the account it belongs to, with what its role allows. + const session = await request.post(`${API}/accounts/servicetoken`, { data: { token } }); + expect(session.ok()).toBeTruthy(); + const { jwt } = await session.json(); + const headers = { Authorization: `Bearer ${jwt}` }; + expect((await request.get(`${API}/documents?page=0&size=1`, { headers })).ok()).toBeTruthy(); + expect( + ( + await request.post(`${API}/documents`, { headers, data: { name: `nope ${name}`, documentFormat: "Json" } }) + ).status(), + ).toBe(403); + + // Never shown again once dismissed. + await drawer.getByRole("button", { name: "Done" }).click(); + await expect(drawer.getByText(token)).toHaveCount(0); + await page.reload(); + await expect(page.getByText(token)).toHaveCount(0); + + await drawer.getByRole("button", { name: "Revoke" }).click(); + await page.getByRole("button", { name: "Revoke token" }).click(); + await expect(drawer.getByText("Revoked", { exact: true })).toBeVisible(); + expect((await request.post(`${API}/accounts/servicetoken`, { data: { token } })).ok()).toBeFalsy(); + + await drawer.getByRole("button", { name: "Remove service account" }).click(); + await page.getByRole("button", { name: "Remove service account" }).last().click(); + await expect(page.getByRole("row", { name: new RegExp(name) })).toHaveCount(0); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts b/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts index 96dc1780..bddf43e5 100644 --- a/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/settings.spec.ts @@ -9,7 +9,7 @@ const DEFAULT_COLOR = "#e3311d"; const brandColorVar = (page: Page) => page.evaluate(() => document.documentElement.style.getPropertyValue("--color-crimson-600").trim()); -/** Sections are real links — a section is a URL you can paste into a ticket — not buttons. */ +/** Sections are real links - a section is a URL you can paste into a ticket - not buttons. */ const sectionLink = (page: Page, name: string) => page.getByRole("link", { name }); /** The hex box beside the colour swatch; `exact` keeps it apart from the picker itself. */ @@ -66,7 +66,7 @@ test("the sign-in page brands itself before anyone has signed in", async ({ page await expect(page.getByText("Unsaved", { exact: true })).toHaveCount(0); await signOut(page); - // No session here at all — the login page reads branding from the anonymous config endpoint. + // No session here at all - the login page reads branding from the anonymous config endpoint. await expect.poll(() => brandColorVar(page)).toBe(INDIGO); await signInAsAdmin(page); diff --git a/SW.Bitween.Web/ClientApp/e2e/sign-out.spec.ts b/SW.Bitween.Web/ClientApp/e2e/sign-out.spec.ts index 76385b4e..a7f78361 100644 --- a/SW.Bitween.Web/ClientApp/e2e/sign-out.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/sign-out.spec.ts @@ -4,12 +4,12 @@ import { ADMIN_EMAIL, ADMIN_PASSWORD } from "./helpers"; /** * Signing out, in the one way that needs a real browser: two tabs. * - * The session lives in four places — a row in the database, the HttpOnly refresh - * cookie, the Jwt in localStorage, and React's own copy. Only the last one decides + * The session lives in four places - a row in the database, the HttpOnly refresh + * cookie, each tab's in-memory Jwt, and React's own copy. Only the last one decides * what you see, and each tab has its own. The other ways a sign-out used to leave * the app on screen are in src/auth/__tests__/SignOut.test.tsx; this one depends on - * the browser delivering a `storage` event from one tab to another, which jsdom - * cannot do. + * the browser delivering a BroadcastChannel message from one tab to another, which + * jsdom cannot do. */ const submitCredentials = async (page: Page) => { @@ -35,13 +35,13 @@ const shell = (page: Page) => page.getByRole("button", { name: "Account menu" }) test("signing out in one tab ends the session in the other", async ({ page, context }) => { await signIn(page); const other = await context.newPage(); - await other.goto("https://localhost:7155/partners"); + await other.goto("partners"); await other.waitForTimeout(2000); expect(await shell(other).count()).toBe(1); await signOut(page); await page.waitForURL(/\/login/, { timeout: 5000 }); // Both credentials are shared by every tab, so the session really has ended here - // too — this tab just never used to find out until someone pressed refresh. + // too - this tab just never used to find out until someone pressed refresh. await other.waitForURL(/\/login/, { timeout: 8000 }); }); diff --git a/SW.Bitween.Web/ClientApp/e2e/subscription-editing.spec.ts b/SW.Bitween.Web/ClientApp/e2e/subscription-editing.spec.ts new file mode 100644 index 00000000..c06c4569 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/e2e/subscription-editing.spec.ts @@ -0,0 +1,217 @@ +import { test, expect, type Page } from "@playwright/test"; +import { AdminApi, stamp } from "./api"; +import { pickOption, signInAsAdmin } from "./helpers"; +import { SEED } from "./seed-data"; + +/** + * Editing a subscription that already exists - the path every change after the first goes down, + * and the one the create specs never reach. Each test saves through the page, reloads it, and + * then asks the API what was stored, so a value the page only appeared to keep is caught. + */ + +/** Published by tools/e2e.sh: a custom handler with versions 1.0.0 and 2.0.0 (current). */ +const CUSTOM_HANDLER = "e2e.samplehandler"; +const CUSTOM_HANDLER_LABEL = "Echo handler (e2e)"; + +interface RawSubscription { + name: string; + handlerId: string | null; + handlerVersion: string | null; + handlerProperties: Record | { key: string; value: string }[]; +} + +/** Handler properties as a plain record, whichever shape the API answered with. */ +const handlerProps = (s: RawSubscription): Record => + Array.isArray(s.handlerProperties) + ? Object.fromEntries(s.handlerProperties.map((p) => [p.key, p.value])) + : s.handlerProperties; + +async function deliverySubscription(api: AdminApi, name: string, { live = false } = {}) { + const documentId = await api.idOf("documents", SEED.informationType); + const partnerId = await api.idOf("partners", SEED.partner); + return api.post("/subscriptions", { + name, + documentId, + type: "ApiCall", + partnerId, + handlerId: "NativeHttpHandler", + handlerProperties: [{ key: "Url", value: "https://example.com/before" }], + receiverProperties: [], + validatorProperties: [], + mapperProperties: [], + documentFilter: [], + // Disabled unless a test sends it something: an enabled one would be live. + inactive: !live, + }); +} + +/** Opens the Delivery step, unless it is already open - a reload keeps the open step (?stage=). */ +async function openDelivery(page: Page) { + const card = page.getByRole("button", { name: /^Delivery/ }); + if ((await card.getAttribute("aria-current")) !== "true") await card.click(); + await expect(card).toHaveAttribute("aria-current", "true"); +} +const saveChanges = async (page: Page) => { + await page.getByRole("button", { name: "Save changes" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); +}; + +test.beforeEach(async ({ page }) => { + await signInAsAdmin(page); +}); + +test("editing an existing subscription's name and delivery settings: the page shows them after a reload, the API stored them, and a secret setting stays masked and is kept until the URL changes", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const name = `Playwright Edit ${stamp()}`; + const id = await deliverySubscription(api, name); + const renamed = `${name} renamed`; + + await page.goto(`subscriptions/${id}`); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + await page.getByRole("textbox", { name: "Name", exact: true }).fill(renamed); + + await openDelivery(page); + await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/before"); + await page.locator("#prop-Url").fill("https://example.com/after"); + await page.locator("#prop-Verb").fill("put"); + await page.locator("#prop-ContentType").fill("application/xml"); + // A secret setting: written here, and never shown or handed back again. + await page.locator("#prop-Headers").fill("X-Api-Key=playwright-secret"); + await saveChanges(page); + + await page.reload(); + await expect(page.getByRole("textbox", { name: "Name", exact: true })).toHaveValue(renamed); + // The stage card summarises what is saved without opening it. + await expect(page.getByRole("button", { name: /^Delivery/ })).toContainText("https://example.com/after"); + await openDelivery(page); + await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/after"); + await expect(page.locator("#prop-Verb")).toHaveValue("put"); + await expect(page.locator("#prop-ContentType")).toHaveValue("application/xml"); + await expect(page.locator("#prop-Headers")).toHaveCount(0); + await expect(page.getByText("playwright-secret")).toHaveCount(0); + + const raw = await api.get(`/subscriptions/${id}`); + expect(raw.name).toBe(renamed); + expect(handlerProps(raw)).toMatchObject({ + Url: "https://example.com/after", + Verb: "put", + ContentType: "application/xml", + }); + expect(handlerProps(raw).Headers, "the API echoed a secret setting back").not.toBe("X-Api-Key=playwright-secret"); + expect(handlerProps(raw).Headers, "the secret setting was not stored").toBeTruthy(); + + // A second edit, to a value already saved once: what is kept is the latest, not the first. + await page.locator("#prop-Verb").fill("patch"); + await saveChanges(page); + const again = handlerProps(await api.get(`/subscriptions/${id}`)); + expect(again.Verb).toBe("patch"); + // Saved again with the secret still masked on the page: it is kept, not blanked. + expect(again.Headers).toBe(handlerProps(raw).Headers); + + // Except when where it is sent changes. A kept secret would go to the new address without the + // editor ever having seen it (AdapterSecretProperties.MayKeepStoredSecrets), so it is dropped and + // has to be typed in again. + await page.locator("#prop-Url").fill("https://example.com/elsewhere"); + await saveChanges(page); + const moved = handlerProps(await api.get(`/subscriptions/${id}`)); + expect(moved.Url).toBe("https://example.com/elsewhere"); + expect(moved.Headers).toBeUndefined(); + + await api.delete(`/subscriptions/${id}`); +}); + +test("discarding edits to an existing subscription puts the saved values back and stores nothing", async ({ + page, + request, +}) => { + const api = await AdminApi.signIn(request); + const name = `Playwright Discard ${stamp()}`; + const id = await deliverySubscription(api, name); + + await page.goto(`subscriptions/${id}`); + await openDelivery(page); + await page.locator("#prop-Url").fill("https://example.com/not-kept"); + await page.getByRole("button", { name: "Discard" }).click(); + await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); + await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/before"); + + expect(handlerProps(await api.get(`/subscriptions/${id}`)).Url).toBe("https://example.com/before"); + + await api.delete(`/subscriptions/${id}`); +}); + +test("pinning a subscription's custom handler to v1.0.0: the picker offers the published versions, the pin survives a reload, an exchange runs through it, and unpinning follows current again", async ({ + page, + request, +}) => { + test.setTimeout(90_000); + const api = await AdminApi.signIn(request); + test.skip( + !(await api.hasAdapter("handlers", CUSTOM_HANDLER)), + `${CUSTOM_HANDLER} is not published here - tools/e2e.sh publishes it; another instance needs it published to test pinning`, + ); + const name = `Playwright Pinned ${stamp()}`; + const id = await deliverySubscription(api, name, { live: true }); + + await page.goto(`subscriptions/${id}`); + await openDelivery(page); + // The built-in picker lists the native adapters; custom ones are a step further in. + await pickOption(page, "handler adapter", /^Custom adapter/); + await pickOption(page, "Custom handler adapter", new RegExp(CUSTOM_HANDLER_LABEL.replace(/[()]/g, "\\$&"))); + // What it runs on is named too: the sample handler is .NET. + await expect(page.getByText("Custom · .NET · v2.0.0")).toBeVisible(); + + const version = page.getByRole("combobox", { name: "Adapter version" }); + await expect(version.locator("option")).toHaveText(["Follow current (v2.0.0)", "v2.0.0", "v1.0.0"]); + await expect(version).toHaveValue(""); + await version.selectOption("1.0.0"); + await saveChanges(page); + + await page.reload(); + await openDelivery(page); + await expect(page.getByRole("combobox", { name: "Adapter version" })).toHaveValue("1.0.0"); + const pinned = await api.get(`/subscriptions/${id}`); + expect(pinned.handlerId).toBe(CUSTOM_HANDLER); + expect(pinned.handlerVersion).toBe("1.0.0"); + + // The Adapters page counts the pin against the version. + await page.goto("adapters?tab=installed"); + const custom = page.getByRole("region", { name: /^Published/ }); + await custom.getByRole("button", { name: new RegExp(CUSTOM_HANDLER_LABEL.replace(/[()]/g, "\\$&")) }).click(); + const v1 = custom.getByRole("row").filter({ hasText: "v1.0.0" }); + await expect(v1.getByRole("cell").nth(3)).toHaveText("1"); // Version, Runtime, Published, Pinned by + await expect(custom.getByRole("row").filter({ hasText: "v2.0.0" })).toContainText("Current"); + + // The pinned package really runs: an exchange sent to the subscription is delivered by it. + await api.sendExchange(id, { shipment: "pinned" }); + await expect + .poll(async () => (await api.exchangesOf(id))[0]?.status ?? null, { + message: "the exchange sent through the pinned handler never finished", + timeout: 60_000, + }) + .toBe(true); + + // Unpinning: back to following whatever is current. + await page.goto(`subscriptions/${id}`); + await openDelivery(page); + await page.getByRole("combobox", { name: "Adapter version" }).selectOption(""); + await saveChanges(page); + expect((await api.get(`/subscriptions/${id}`)).handlerVersion).toBeNull(); + + await api.delete(`/subscriptions/${id}`); +}); + +test("a native handler on an existing subscription offers no version to pin", async ({ page, request }) => { + const api = await AdminApi.signIn(request); + const id = await deliverySubscription(api, `Playwright Native ${stamp()}`); + + await page.goto(`subscriptions/${id}`); + await openDelivery(page); + await expect(page.getByText("Runs in-process")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Adapter version" })).toHaveCount(0); + + await api.delete(`/subscriptions/${id}`); +}); diff --git a/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts b/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts index 6143f28b..ba802d2e 100644 --- a/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts @@ -1,15 +1,17 @@ import { test, expect } from "@playwright/test"; -import { pickOption } from "./helpers"; +import { pickOption, signInAsAdmin, SUBSCRIPTION_PAGE } from "./helpers"; +import { SEED } from "./seed-data"; -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +/** + * What the pickers and stage cards call the two HTTP adapters once one is chosen. Each adapter's + * manifest (SW.Bitween.NativeAdapters/HttpReceiver/adapter.json and its siblings) gives it a display name, and that is what is + * shown; the id with "Native" stripped is only the fallback for an adapter that has none. + */ +const RECEIVER_LABEL = "HTTP poll"; +const HANDLER_LABEL = "HTTP request"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("scheduled job create, adapters, pause/resume, receive now, list, delete", async ({ page }) => { @@ -19,13 +21,13 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", await page.fill("#nj-name", name); // The information type is a searchable picker, and the stages below it are cards that open - // in place — there is no wizard to "Continue" through any more. - await pickOption(page, "Information type", /Shipment order/); - await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(/Shipment order/); + // in place - there is no wizard to "Continue" through any more. + await pickOption(page, "Information type", new RegExp(SEED.informationType)); + await expect(page.getByRole("combobox", { name: "Information type" })).toHaveValue(new RegExp(SEED.informationType)); - // Source — open by default. Receiver adapter plus its one required prop. + // Source - open by default. Receiver adapter plus its one required prop. await pickOption(page, "receiver adapter", "NativeHttpReceiver"); - await expect(page.getByRole("combobox", { name: "receiver adapter" })).toHaveValue("HttpReceiver"); + await expect(page.getByRole("combobox", { name: "receiver adapter" })).toHaveValue(RECEIVER_LABEL); await page.locator("#prop-Url").fill("https://example.com/feed"); await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/feed"); @@ -33,20 +35,20 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", // keeps #prop-Url unambiguous below. await page.getByRole("button", { name: "Close this step" }).click(); - // Delivery — handler adapter and its required prop (transformation stays "Passes through"). + // Delivery - handler adapter and its required prop (transformation stays "Passes through"). // Only one stage is open at a time, so #prop-Url is unambiguous here. await page.getByRole("button", { name: /^Delivery/ }).click(); await pickOption(page, "handler adapter", "NativeHttpHandler"); - await expect(page.getByRole("combobox", { name: "handler adapter" })).toHaveValue("HttpHandler"); + await expect(page.getByRole("combobox", { name: "handler adapter" })).toHaveValue(HANDLER_LABEL); await page.locator("#prop-Url").fill("https://example.com/sink"); await expect(page.locator("#prop-Url")).toHaveValue("https://example.com/sink"); // "Enable immediately" is checked by default. await page.getByRole("button", { name: "Create job" }).click(); - await expect(page).toHaveURL(/\/subscriptions\/\d+$/); + await expect(page).toHaveURL(SUBSCRIPTION_PAGE); await expect(page.getByRole("heading", { name })).toBeVisible(); - // A badge now, not a button — the switch that turns it off lives with Pause on the right. + // A badge now, not a button - the switch that turns it off lives with Pause on the right. await expect(page.getByText("Active", { exact: true }).first()).toBeVisible(); // Disable / enable. Distinct from pause below: this one drops the work rather than holding it, @@ -89,13 +91,13 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", // summarises what it saved, so both the adapter and its property show without opening it. await page.reload(); const source = page.getByRole("button", { name: /^Source/ }); - await expect(source).toContainText("HttpReceiver"); + await expect(source).toContainText(RECEIVER_LABEL); await expect(source).toContainText("https://example.com/feed"); const delivery = page.getByRole("button", { name: /^Delivery/ }); - await expect(delivery).toContainText("HttpHandler"); + await expect(delivery).toContainText(HANDLER_LABEL); await expect(delivery).toContainText("https://example.com/sink"); - // Narrow by type — the supported filter — so the row can't be paged out of sight. Deliberately + // Narrow by type - the supported filter - so the row can't be paged out of sight. Deliberately // not the search box: a multi-word term is encoded with "+" and the backend never decodes it, // so searching any name containing a space returns nothing (pre-existing, see notes). await page.goto("subscriptions?type=Receiving"); @@ -104,10 +106,10 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", await expect(row.getByText("undefined")).toHaveCount(0); // The name, not the row's centre. The whole row opens the subscription, but it also - // carries links of its own — information type, partner — and which one sits under the + // carries links of its own - information type, partner - and which one sits under the // centre depends on how wide the columns happen to be. await row.getByText(name).click(); - await expect(page).toHaveURL(/\/subscriptions\/\d+$/); + await expect(page).toHaveURL(SUBSCRIPTION_PAGE); await page.getByRole("button", { name: "Delete" }).click(); await page.getByRole("button", { name: "Delete subscription" }).click(); await expect(page).toHaveURL(/\/subscriptions$/); diff --git a/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts b/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts index 9f8f0120..7717c277 100644 --- a/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts @@ -7,7 +7,7 @@ import { signInAsAdmin } from "./helpers"; * These are all data-shape problems: a customer with 60-character subscription * names, a 4KB minified payload and 45 subscriptions on one information type. * None of that exists in a dev database, so every test here rewrites the API - * response on the way past rather than seeding rows — nothing is written, and + * response on the way past rather than seeding rows - nothing is written, and * the assertions don't drift with whatever the local data happens to be. */ @@ -59,7 +59,7 @@ const overflowing = (page: Page) => .filter((r) => r.over > 1), ); -/** Leaf elements whose text is cut off — an ellipsis the reader can't get past. */ +/** Leaf elements whose text is cut off - an ellipsis the reader can't get past. */ const clipped = (page: Page) => page.evaluate(() => [...document.querySelectorAll("td")] @@ -93,7 +93,7 @@ test("long names wrap rather than collapsing into a row of ellipses", async ({ p for (const slug of ["subscriptions", "aggregations", "scheduled-jobs"]) { await page.goto(slug); await page.waitForTimeout(1500); - // Exceptions and joined key lists are allowed to clip — they carry a title. + // Exceptions and joined key lists are allowed to clip - they carry a title. // A name never is. const cut = (await clipped(page)).filter((t) => LONG_NAMES.some((n) => n.startsWith(t.replace(/…$/, "")))); expect(cut, `${slug} clipped a name`).toEqual([]); @@ -113,23 +113,32 @@ test("a long payload doesn't stretch the exchanges table", async ({ page }) => { route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ key: "k", data: payload }) })); await page.goto("exchanges"); - await page.waitForTimeout(1200); - const before = await page.evaluate(() => document.querySelector("table")!.scrollWidth); + // Waits for the rows rather than a fixed time: on a busy machine the list was still loading + // after 1.2s, and there was no table to measure. + await expect(page.locator("tbody tr").first()).toBeVisible({ timeout: 15000 }); - // The expander, not the row: the row's cells carry links of their own. + // The last cell, not the first: the row's other cells carry links of their own. await page.locator("tbody tr").first().locator("td").last().click(); await expect(page.getByRole("button", { name: "Download this document" })).toBeVisible(); + // The document opens in the panel beside the list. However long its one line, it must not + // widen the panel past its own edge, nor the list beside it. + const panelOverflow = () => + page.evaluate(() => { + const panel = document.querySelector("aside[aria-label^='Exchange ']")!; + return panel.scrollWidth - panel.clientWidth; + }); + // Laid out over lines, and the one line of minified JSON never sets the width. await expect(page.getByText('"Shipment": {')).toBeVisible(); expect(await overflowing(page)).toEqual([]); - expect(await page.evaluate(() => document.querySelector("table")!.scrollWidth)).toBe(before); + expect(await panelOverflow()).toBeLessThanOrEqual(1); // Raw shows it exactly as it arrived, and still can't stretch anything. await page.getByRole("button", { name: "Raw" }).click(); await expect(page.getByText('{"Shipment":{"Uid":"STF.365673"', { exact: false })).toBeVisible(); expect(await overflowing(page)).toEqual([]); - expect(await page.evaluate(() => document.querySelector("table")!.scrollWidth)).toBe(before); + expect(await panelOverflow()).toBeLessThanOrEqual(1); }); test("a long panel list keeps its Type column inside the card", async ({ page }) => { @@ -148,7 +157,7 @@ test("a long panel list keeps its Type column inside the card", async ({ page }) await page.goto("information-types"); // The route above can only multiply a subscription that exists, so open a type something - // uses — the first row is just whichever type was made last. + // uses - the first row is just whichever type was made last. const usedBy = page .locator('a[href*="/subscriptions/"]') .or(page.getByRole("button", { name: /^Show all \d+ subscriptions$/ })); diff --git a/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts b/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts index cfd96d0f..c9704bb4 100644 --- a/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/team-members.spec.ts @@ -22,7 +22,7 @@ test("add a member, they can sign in, then remove them", async ({ page }) => { await expect(row).toContainText("Viewer"); await expect(row).toContainText("Active"); - // The account is real from the first moment — no accept step in between. + // The account is real from the first moment - no accept step in between. await signOut(page); await signIn(page, email, FIRST_PASSWORD); await expect(page.getByRole("button", { name: "Account menu" })).toContainText("New Joiner"); diff --git a/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts b/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts index 1bab8588..7dd26a94 100644 --- a/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/work-groups.spec.ts @@ -1,14 +1,8 @@ import { test, expect } from "@playwright/test"; - -const ADMIN_EMAIL = "admin@Bitween.systems"; -const ADMIN_PASSWORD = "Mtm@dmin!2"; +import { signInAsAdmin } from "./helpers"; test.beforeEach(async ({ page }) => { - await page.goto("login"); - await page.fill("#login-email", ADMIN_EMAIL); - await page.fill("#login-password", ADMIN_PASSWORD); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + await signInAsAdmin(page); }); test("work group create, edit queue settings, list, delete", async ({ page }) => { @@ -16,14 +10,16 @@ test("work group create, edit queue settings, list, delete", async ({ page }) => // Creating happens in a dialog on the list page, not on a page of its own. await page.goto("work-groups"); - await page.getByRole("button", { name: "New work group" }).click(); + await page.getByRole("button", { name: "New work group" }).first().click(); const dialog = page.getByRole("dialog", { name: "New work group" }); // `exact` keeps this off "Bus message name", which also contains "Name". await dialog.getByRole("textbox", { name: "Name", exact: true }).fill(name); await dialog.getByRole("button", { name: "Create work group" }).click(); - await expect(page).toHaveURL(/\/work-groups\/\d+$/); + // Generous on purpose: creating a group restarts every node's consumers so they pick up its new + // queue, and the request only returns once that is done - several seconds on a cold instance. + await expect(page).toHaveURL(/\/work-groups\/\d+$/, { timeout: 15000 }); await expect(page.getByRole("heading", { name })).toBeVisible(); // Defaults carried over from the create dialog. await expect(page.locator("#wg-prefetch")).toHaveValue("10"); @@ -33,7 +29,7 @@ test("work group create, edit queue settings, list, delete", async ({ page }) => await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByRole("button", { name: "Save changes" })).toHaveCount(0); - // Reload forces a fresh GET — WorkGroups/Search.cs now reads the DB + // Reload forces a fresh GET - WorkGroups/Search.cs now reads the DB // directly (not IInfolinkCache), so this reflects the update immediately. await page.reload(); await expect(page.locator("#wg-prefetch")).toHaveValue("25"); @@ -44,7 +40,7 @@ test("work group create, edit queue settings, list, delete", async ({ page }) => // Nothing is assigned to a brand-new group, so "Used by" is an em dash. Its consumer // count is deliberately not asserted: declaring the group's queue makes the running // instance a consumer of it straight away, so "Nodes" is legitimately 1, not blank. - await expect(row.getByRole("cell").nth(2)).toHaveText("—"); + await expect(row.getByRole("cell").nth(2)).toHaveText("-"); await row.getByRole("button", { name: `Open ${name}` }).click(); await expect(page).toHaveURL(/\/work-groups\/\d+$/); diff --git a/SW.Bitween.Web/ClientApp/package.json b/SW.Bitween.Web/ClientApp/package.json index e2a49f62..dfc6e1c2 100644 --- a/SW.Bitween.Web/ClientApp/package.json +++ b/SW.Bitween.Web/ClientApp/package.json @@ -13,8 +13,13 @@ }, "dependencies": { "@azure/msal-browser": "^4.0.1", - "@babel/runtime": "^8.0.0", - "@codemirror/language": "^6.12.4", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/language": "^6.13.1", + "@codemirror/legacy-modes": "^6.5.5", + "@codemirror/merge": "^6.12.2", "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", "@fontsource-variable/instrument-sans": "^5.2.8", @@ -28,9 +33,10 @@ "highlight.js": "^11.12.0", "immer": "^11.1.8", "lucide-react": "^1.24.0", + "monaco-editor": "^0.57.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router": "^8.3.0", + "react-router": "^8.3.1", "tailwindcss": "^4.3.2" }, "devDependencies": { diff --git a/SW.Bitween.Web/ClientApp/playwright.config.ts b/SW.Bitween.Web/ClientApp/playwright.config.ts index 81c5ca1c..46448f26 100644 --- a/SW.Bitween.Web/ClientApp/playwright.config.ts +++ b/SW.Bitween.Web/ClientApp/playwright.config.ts @@ -1,17 +1,26 @@ import { defineConfig } from "@playwright/test"; +import { BASE_URL } from "./e2e/env"; -// Points at the locally running backend (SW.Bitween.Web.Local), which serves -// the built SPA at the site root — there is no separate dev server to boot. +// Points at a running backend, which serves the built SPA at the site root - there is no +// separate dev server to boot. tools/e2e.sh starts a throwaway one and runs this against it; +// on its own, this runs against whatever answers at E2E_BASE_URL (https://localhost:7155/). export default defineConfig({ testDir: "./e2e", fullyParallel: false, workers: 1, reporter: "list", - // Tests run against a real database, so a failed run leaves data behind. This clears it and - // repairs the admin's roles before anything else, so one failure can't cascade into the next run. - globalSetup: "./e2e/global-setup.ts", use: { - baseURL: "https://localhost:7155/", + baseURL: BASE_URL, ignoreHTTPSErrors: true, + // Kept for failures only, so a red run says what the page looked like without every green + // one paying for a trace. + trace: "retain-on-failure", }, + projects: [ + // Tests run against a real database, so a failed run leaves data behind, and a new database + // has none of the rows the specs build on. This clears the one and creates the other before + // anything else - see e2e/seed.setup.ts. + { name: "seed", testMatch: /seed\.setup\.ts/ }, + { name: "chromium", dependencies: ["seed"] }, + ], }); diff --git a/SW.Bitween.Web/ClientApp/public/blank.html b/SW.Bitween.Web/ClientApp/public/blank.html index e6aa7982..c0c7fa5b 100644 --- a/SW.Bitween.Web/ClientApp/public/blank.html +++ b/SW.Bitween.Web/ClientApp/public/blank.html @@ -13,7 +13,7 @@ This must not be the app itself: index.html boots the SPA, RequireAuth finds no session and does ``, and that navigation - drops the fragment before the opener can read it — leaving the popup sitting + drops the fragment before the opener can read it - leaving the popup sitting on a login page and the sign-in failing. Keep this page empty and script-free; nothing here has to run for the flow to complete. --> diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/noLongDash.test.ts b/SW.Bitween.Web/ClientApp/src/__tests__/noLongDash.test.ts new file mode 100644 index 00000000..df80607f --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/__tests__/noLongDash.test.ts @@ -0,0 +1,38 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +/** + * The long dash (em dash, en dash, horizontal bar) is not used anywhere in Bitween: not in what + * the UI says, not in code, comments, docs or tests. A spaced one becomes " - ", any other "-". + * This walks the whole repository, so a new one fails here with the file and line that has it. + */ +const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "../../../.."); +const SKIP = new Set(["node_modules", "bin", "obj", "wwwroot", ".git", "test-results", "playwright-report", "dist"]); +const TEXT = /\.(cs|csproj|props|targets|ts|tsx|js|mjs|cjs|json|md|yml|yaml|css|html|sh|ps1|py|txt|xml|cshtml|sql|toml)$/i; +// Built from character codes, so this file has none of them in it. +const LONG_DASH = new RegExp(`[${String.fromCharCode(0x2013, 0x2014, 0x2015)}]`); + +function* files(dir: string): Generator { + for (const name of readdirSync(dir)) { + if (SKIP.has(name)) continue; + const path = join(dir, name); + if (statSync(path).isDirectory()) yield* files(path); + else if (TEXT.test(name)) yield path; + } +} + +describe("the long dash", () => { + it("appears nowhere in the repository", () => { + const found: string[] = []; + for (const path of files(ROOT)) { + readFileSync(path, "utf8") + .split("\n") + .forEach((line, i) => { + if (LONG_DASH.test(line)) found.push(`${relative(ROOT, path)}:${i + 1}`); + }); + } + expect(found).toEqual([]); + }); +}); diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/pages.test.ts b/SW.Bitween.Web/ClientApp/src/__tests__/pages.test.ts new file mode 100644 index 00000000..518d0ed7 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/__tests__/pages.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { PAGES, pageTrail, pathOf, type PageId } from "../pages"; +import { NAV_GROUPS } from "../nav"; + +const ids = Object.keys(PAGES) as PageId[]; + +describe("the page registry", () => { + it("gives every page its own path", () => { + const paths = ids.map((id) => PAGES[id].path); + expect(new Set(paths).size).toBe(paths.length); + }); + + it("puts every page under a parent whose path starts its own, so a breadcrumb can be filled in", () => { + for (const id of ids) { + const parent = PAGES[id].parent; + if (parent) expect(PAGES[id].path.startsWith(`${PAGES[parent].path}/`), `${id} under ${parent}`).toBe(true); + } + }); + + it("guards every page in the sidebar with a permission", () => { + for (const item of NAV_GROUPS.flatMap((g) => g.items)) expect(item.permissions, item.id).not.toHaveLength(0); + }); + + it("walks a page's trail and fills in its path", () => { + expect(pageTrail("gatewaySubscriptionNew")).toEqual([ + "apiGateways", + "apiGateway", + "attachPartner", + "gatewaySubscriptionNew", + ]); + expect(pathOf("apiGateway", { id: "15" })).toBe("/api-gateways/15"); + expect(pathOf("apiGateway")).toBeNull(); + }); +}); diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/support/listPage.ts b/SW.Bitween.Web/ClientApp/src/__tests__/support/listPage.ts new file mode 100644 index 00000000..d3ceb9e1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/__tests__/support/listPage.ts @@ -0,0 +1,19 @@ +import { http, HttpResponse, type JsonBodyType } from "msw"; +import { apiPath } from "./renderApp"; + +/** A search endpoint's page of rows. */ +export const page = (rows: unknown[]) => ({ result: rows, totalCount: rows.length }); +export const noRows = page([]); + +/** GET `path` answering `body`, and every URL it was asked with, so a test can see what was searched. */ +export function recorded(path: string, body: JsonBodyType) { + const asked: string[] = []; + const handler = http.get(apiPath(path), ({ request }) => { + asked.push(decodeURIComponent(request.url)); + return HttpResponse.json(body); + }); + return { handler, asked }; +} + +export const empty = (...paths: string[]) => paths.map((p) => http.get(apiPath(p), () => HttpResponse.json(noRows))); +export const none = (...paths: string[]) => paths.map((p) => http.get(apiPath(p), () => HttpResponse.json([]))); diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/support/metricsFixtures.ts b/SW.Bitween.Web/ClientApp/src/__tests__/support/metricsFixtures.ts new file mode 100644 index 00000000..8ba4b311 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/__tests__/support/metricsFixtures.ts @@ -0,0 +1,108 @@ +import { http, HttpResponse } from "msw"; +import type { ExchangeSummary, Insights, MetricsOverview } from "../../api/metricsTypes"; + +// Its own copy of renderApp's helper: renderApp imports this module for its defaults. +const apiPath = (path: string) => `*/api${path}`; + +export const summary = (over: Partial = {}): ExchangeSummary => ({ + total: 0, + succeeded: 0, + badResponse: 0, + failed: 0, + retries: 0, + manualRetries: 0, + successRate: null, + p50Ms: null, + p95Ms: null, + bytes: 0, + ...over, +}); + +/** GET /metrics/overview on an instance with nothing yet, with whatever a test sets. */ +export const overview = (over: Partial = {}): MetricsOverview => ({ + since: null, + hourly: [], + weekAgo: [], + last24h: summary(), + previous24h: summary(), + last7d: summary(), + daily: [], + topFailing: [], + busiestSubscriptions: [], + busiestPartners: [], + slowestAdapters: [], + silent: [], + failing: 0, + degraded: 0, + backlog: [], + ...over, +}); + +/** The dashboard's metrics endpoints, answering `overview` and `insights`. */ +export const metricsHandlers = ( + o: MetricsOverview = overview(), + insights: Insights = { since: null, insights: [] }, +) => [ + http.get(apiPath("/metrics/overview"), () => HttpResponse.json(o)), + http.get(apiPath("/metrics/insights"), () => HttpResponse.json(insights)), +]; + +/** + * Every metrics endpoint answering "nothing counted yet", which renderApp installs for every test + * (a test's own handlers come first and win). Pages show metrics beside what they are about, and + * a test about something else shouldn't have to answer for them. + */ +export const defaultMetrics = () => [ + ...metricsHandlers(), + http.get(apiPath("/metrics/adapters"), ({ request }) => + HttpResponse.json({ + since: null, + days: Number(new URL(request.url).searchParams.get("days") ?? 7), + adapters: [], + quiet: [], + }), + ), + http.get(apiPath("/metrics/adapter"), ({ request }) => + HttpResponse.json({ + id: new URL(request.url).searchParams.get("id"), + days: 7, + since: null, + total: { + calls: 0, + failures: 0, + failureRate: null, + timeouts: 0, + limitStops: 0, + crashes: 0, + p50Ms: null, + p95Ms: null, + maxMs: 0, + }, + versions: [], + nodes: [], + roles: [], + daily: [], + errors: [], + subscriptions: [], + }), + ), + http.get(apiPath("/metrics/subscriptions"), () => HttpResponse.json({ since: null, days: 7, subscriptions: [] })), + http.get(apiPath("/metrics/subscription"), ({ request }) => + HttpResponse.json({ + id: Number(new URL(request.url).searchParams.get("id")), + days: 7, + since: null, + status: null, + lastExchangeHour: null, + quietHours: null, + usualGapHours: null, + total: { ...summary(), failedAtMapper: 0, failedAtHandler: 0 }, + hourly: [], + daily: [], + nodes: [], + errors: [], + }), + ), + http.get(apiPath("/metrics/partners"), () => HttpResponse.json({ since: null, days: 7, partners: [] })), + http.get(apiPath("/metrics/nodes"), () => HttpResponse.json({ nodes: [] })), +]; diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/support/renderApp.tsx b/SW.Bitween.Web/ClientApp/src/__tests__/support/renderApp.tsx index ad02d2d4..0c050edc 100644 --- a/SW.Bitween.Web/ClientApp/src/__tests__/support/renderApp.tsx +++ b/SW.Bitween.Web/ClientApp/src/__tests__/support/renderApp.tsx @@ -5,11 +5,12 @@ import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse, type RequestHandler } from "msw"; import { RouterProvider, createMemoryRouter } from "react-router"; -import { TOKEN_KEY } from "../../api"; +import { setToken } from "../../api/http/request"; import type { AppConfig } from "../../api/http/appConfig"; import { SessionProvider } from "../../auth/SessionContext"; import { routes } from "../../router"; import { server } from "./server"; +import { defaultMetrics } from "./metricsFixtures"; /** * Every permission key there is, read from the C# catalog rather than copied, so an administrator @@ -62,15 +63,23 @@ export interface RenderAppOptions { } /** - * Mounts the whole app — session, router, data layer — at one URL, against the mock network. + * Mounts the whole app - session, router, data layer - at one URL, against the mock network. * * The real routes and the real API client run; only the server is fake. So a test here reads like * the Playwright spec it replaced, minus the backend and the database that made those slow and * dependent on whatever data happened to be lying around. */ export function renderApp(path: string, { as = {}, config = {}, handlers = [] }: RenderAppOptions = {}) { - if (as) localStorage.setItem(TOKEN_KEY, "test-token"); - server.use(...handlers, appConfig(config), ...(as ? [profile(as)] : [])); + if (as) setToken("test-token"); + server.use( + ...handlers, + appConfig(config), + ...(as ? [profile(as)] : []), + // A lookup most subscription screens make and few tests care about: none, unless a test says. + http.get(apiPath("/subscriptioncategories"), () => HttpResponse.json({ result: [], totalCount: 0 })), + // Metrics, which many pages show beside what they are about: none yet, unless a test says. + ...defaultMetrics(), + ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const router = createMemoryRouter(routes, { initialEntries: [path] }); diff --git a/SW.Bitween.Web/ClientApp/src/__tests__/support/setup.ts b/SW.Bitween.Web/ClientApp/src/__tests__/support/setup.ts index 320a5238..cd799d8b 100644 --- a/SW.Bitween.Web/ClientApp/src/__tests__/support/setup.ts +++ b/SW.Bitween.Web/ClientApp/src/__tests__/support/setup.ts @@ -1,15 +1,45 @@ import "@testing-library/jest-dom/vitest"; -import { cleanup } from "@testing-library/react"; -import { afterAll, afterEach, beforeAll } from "vitest"; +import { clearToken } from "../../api/http/request"; +import { cleanup, configure } from "@testing-library/react"; +import { afterAll, afterEach, beforeAll, vi } from "vitest"; import { resetAppConfig } from "../../api/http/appConfig"; import { server } from "./server"; +// jsdom has no ResizeObserver, and Headless UI's combobox reaches for one as it closes. Unstubbed, +// that throws after the test has already passed, and vitest reports it against whichever test ran. +globalThis.ResizeObserver ??= class { + observe() {} + unobserve() {} + disconnect() {} +}; + +// How long a findBy waits for the page. The 1s default is a fast machine with nothing else to do; +// under a full parallel run a page could take longer than that to answer, and the test failed +// on a page that was right, only slow. +configure({ asyncUtilTimeout: 4_000 }); + +// Monaco needs a real browser: canvas measurement, web workers. In jsdom the adapter editor gets a +// plain text area in its place, wired the same way, so a test can read and type a file's text. +vi.mock("../../components/code/CodeEditor", async () => { + const { createElement } = await import("react"); + return { + default: (props: { active: string | null; files: Record; onChange: (path: string, text: string) => void }) => + props.active + ? createElement("textarea", { + "aria-label": `Code of ${props.active}`, + value: props.files[props.active] ?? "", + onChange: (e: { target: { value: string } }) => props.onChange(props.active!, e.target.value), + }) + : null, + }; +}); + /** Requests the page made that no handler answered, collected so the test can fail on them. */ const unanswered: string[] = []; beforeAll(() => { // Printing alone isn't enough: vitest hides a passing test's output, and the page just renders - // its error state — which can satisfy an assertion as easily as the real thing would. + // its error state - which can satisfy an assertion as easily as the real thing would. server.listen({ onUnhandledRequest: (request, print) => { const url = new URL(request.url); @@ -33,6 +63,8 @@ afterEach(() => { cleanup(); server.resetHandlers(); localStorage.clear(); + // The Jwt lives in the request module's memory, which outlives a test like the page would. + clearToken(); // Unsaved drafts (the settings page's, for one) live here, and would leak into the next test. sessionStorage.clear(); // Fetched once per page load and cached for the life of the module; each test is a new page. @@ -44,3 +76,10 @@ afterEach(() => { }); afterAll(() => server.close()); + +// The router loads every page lazily. Loaded here once, each test's page resolves at once instead +// of being transformed on its first render, which under a full parallel run could take longer +// than a findBy waits. +import.meta.glob(["/src/pages/**/*.tsx", "!/src/pages/**/__tests__/**", "/src/components/nativeMapper/MapperEditorRoute.tsx"], { + eager: true, +}); diff --git a/SW.Bitween.Web/ClientApp/src/api/client.ts b/SW.Bitween.Web/ClientApp/src/api/client.ts index 93fd7430..133d780d 100644 --- a/SW.Bitween.Web/ClientApp/src/api/client.ts +++ b/SW.Bitween.Web/ClientApp/src/api/client.ts @@ -14,6 +14,7 @@ import type { BusGatewayRow, DataSourceProvider, GatewayAuthentication, + GatewayFailureAnswer, DataSourceDetail, DataSourceInspectResult, DataSourceRow, @@ -22,6 +23,7 @@ import type { DataSourceTelemetry, DataSourceTestResult, DashboardData, + ExchangeBreakdown, ExchangeQuery, ExchangeRow, ExportFilesCheck, @@ -51,8 +53,11 @@ import type { PartnerRow, PermissionArea, PermissionKey, + IssuedServiceToken, + ServiceAccount, QueueHealthSnapshot, UnattendedDeleteResult, + DeadLetterMessage, ReceiveAttemptRow, SourcePaths, ReceiveOutcome, @@ -74,13 +79,37 @@ import type { RetentionProposal, RetentionStatus, SettingRow, + InstanceAbout, + SubscriptionCategory, + BackgroundWork, + GatewayRejections, + AdapterUploadResult, + ClusterNodes, User, WorkGroup, WorkGroupDetail, TrailEntry, WorkGroupRow, + AdapterSourceFile, + AdapterSourceListing, + AdapterDraft, + AdapterDraftSummary, + DraftBuild, + DraftLanguage, + DraftPublishResult, + DraftRun, } from "./types"; import type { SaveResult } from "./http/dataSourceStatements"; +import type { + AdapterUsageDetail, + AdapterUsageList, + Insights, + MetricsOverview, + NodeMetrics, + PartnerUsageList, + SubscriptionHealthList, + SubscriptionMetrics, +} from "./metricsTypes"; /** * The single data-access contract the UI is written against, implemented by @@ -88,24 +117,33 @@ import type { SaveResult } from "./http/dataSourceStatements"; * so an endpoint's shape can change in one place. */ export interface ApiClient { - // — session — + // - session - getSession(): Promise; login(email: string, password: string): Promise; loginWithMicrosoft(): Promise; logout(): Promise; - // — self service — - // No password-reset flow exists on the backend yet (BACKEND_WIRING_PLAN.md G3) — it needs + // - self service - + // No password-reset flow exists on the backend yet (BACKEND_WIRING_PLAN.md G3) - it needs // outbound mail, which Bitween doesn't have. Hidden in the UI rather than faked. updateProfile(changes: { displayName: string }): Promise; changePassword(currentPassword: string, newPassword: string): Promise; - // — members — + // - service accounts - + listServiceAccounts(): Promise; + createServiceAccount(input: { name: string; roleIds: number[] }): Promise; + updateServiceAccount(id: number, input: { name: string; roleIds: number[] }): Promise; + setServiceAccountDisabled(id: number, disabled: boolean): Promise; + deleteServiceAccount(id: number): Promise; + issueServiceToken(id: number, input: { name: string; expiresInDays: number }): Promise; + revokeServiceToken(id: number, tokenId: number): Promise; + + // - members - listUsers(): Promise; getUser(id: string): Promise; /** * Creates a member outright, password and all. Bitween has no outbound mail, so there's no - * invite link — whoever adds the member passes the first password on themselves. + * invite link - whoever adds the member passes the first password on themselves. */ createUser(input: { displayName: string; @@ -121,7 +159,7 @@ export interface ApiClient { setUserDisabled(id: string, disabled: boolean): Promise; deleteUser(id: string): Promise; - // — roles — + // - roles - /** The catalog the backend enforces, so the role matrix can never offer a grant it ignores. */ getPermissionCatalog(): Promise; listRoles(): Promise; @@ -133,7 +171,7 @@ export interface ApiClient { ): Promise; deleteRole(id: string): Promise; - // — partners — + // - partners - listPartners(): Promise; searchPartners(query: { search: string; offset: number; limit: number }): Promise>; getPartner(id: number): Promise; @@ -160,7 +198,7 @@ export interface ApiClient { addPartnerCredential(id: number, name: string): Promise<{ key: string }>; revokePartnerCredential(id: number, name: string): Promise; - // — information types — + // - information types - listInformationTypes(): Promise; searchInformationTypes(query: { search: string; @@ -172,7 +210,7 @@ export interface ApiClient { getInformationType(id: number): Promise; /** Same payload as update: a new type arrives complete, promoted properties included. */ // `retiredOn` and `isSystem` are absent from both on purpose: retiring is its own command - // and being built-in is the backend's to say, so neither is a field a save can carry — + // and being built-in is the backend's to say, so neither is a field a save can carry - // otherwise a form that round-trips a type could change them by accident. createInformationType( input: Omit, @@ -185,7 +223,7 @@ export interface ApiClient { /** Toggles: retires a type that is in use, restores one that is retired. */ retireInformationType(id: number): Promise<{ retiredOn: string | null }>; - // — global values — + // - global values - listValueSets(): Promise; getValueSet(id: string): Promise; createValueSet(input: { @@ -200,16 +238,17 @@ export interface ApiClient { ): Promise; deleteValueSet(id: string): Promise; - // — subscriptions (light summaries; cache aggressively) — + // - subscriptions (light summaries; cache aggressively) - listSubscriptions(): Promise; - // — subscriptions — + // - subscriptions - listSubscriptionRows(): Promise; searchSubscriptionRows(query: { search: string; type: SubscriptionType | null; informationTypeId?: number | null; partnerId?: number | null; + categoryId?: number | null; inactive?: boolean | null; offset: number; limit: number; @@ -220,7 +259,7 @@ export interface ApiClient { type: SubscriptionType; name: string; informationTypeId: number; - /** Required by the types that carry their own partner — Internal and ApiCall. */ + /** Required by the types that carry their own partner - Internal and ApiCall. */ partnerId?: number | null; /** Aggregation only: whose exchanges get rolled up. Required, and fixed once created. */ aggregationForId?: number | null; @@ -269,6 +308,12 @@ export interface ApiClient { | "responseMessageTypeName" | "runOnBadResponses" | "aggregationTarget" + | "autoPauseAfterFailures" + | "expectTraffic" + | "receiverVersion" + | "validatorVersion" + | "mapperVersion" + | "handlerVersion" > > & { /** Create this response subscription in the same save and hand the response to it. */ @@ -279,6 +324,7 @@ export interface ApiClient { /** Toggles paused: paused subscriptions accept work but hold it. */ pauseSubscription(id: number): Promise; receiveNow(id: number): Promise; + clearRunning(id: number): Promise<{ cleared: boolean }>; /** Runs an aggregation's roll-up now instead of waiting for its schedule. */ aggregateNow(id: number): Promise; /** Run history for one scheduled subscription, newest first. Empty for unscheduled types. */ @@ -289,13 +335,41 @@ export interface ApiClient { ): Promise>; /** The paths in the last document a subscription received. Needs exchanges.view. */ getSourcePaths(subscriptionId: number): Promise; - /** Newest run of every scheduled subscription — one request for a whole list. */ + /** Newest run of every scheduled subscription - one request for a whole list. */ listLastRuns(): Promise; /** Will these schedules actually fire? Asks the scheduler, not the subscription record. */ listScheduleHealth(): Promise; listAdapters(kind: AdapterKind): Promise; + /** The source files a published version carries. Needs adapter-source.view. */ + getAdapterSource(adapterId: string, version: string): Promise; + /** One source file of a published version; each read is audited. Needs adapter-source.view. */ + getAdapterSourceFile(adapterId: string, version: string, path: string): Promise; + + // - the adapter editor (adapter-source.edit; publishing and promoting need adapter-source.operate) - + listAdapterDrafts(): Promise; + getAdapterDraft(id: number): Promise; + /** A new adapter from the template; resolves to the draft's id. */ + createAdapterDraft(input: { name: string; language: DraftLanguage; kind: string; adapterId?: string }): Promise; + /** A draft of a published Python or Node version, from its source. */ + draftFromVersion(adapterId: string, version: string): Promise; + /** + * Saves the files. With `baseHash`, the draft's hash when it was opened or last saved here: a + * save over someone else's since then is refused with DRAFT_CHANGED. Without it, saved regardless. + */ + saveAdapterDraft(id: number, files: Record, baseHash?: string): Promise; + deleteAdapterDraft(id: number): Promise; + /** Builds it and checks it against its contracts with these settings, which are never stored. */ + buildAdapterDraft(id: number, settings: Record, buildOnly?: boolean): Promise; + tryAdapterDraft(id: number, settings: Record, command: string, input: string): Promise; + publishAdapterDraft(id: number, input: { version: string; releaseNotes: string; settings: Record }): Promise; + /** Makes a published version the one that runs wherever nothing is pinned. */ + promoteAdapter(adapterId: string, version: string): Promise; + /** Takes a version out of use: still listed and still run where pinned, never pinned or made current again. */ + withdrawAdapterVersion(adapterId: string, version: string): Promise; + /** Publishes a package built by bitween adapter build, any language. */ + uploadAdapterPackage(file: Blob, options: { version: string; current: boolean; releaseNotes: string }): Promise; - // — work groups — + // - work groups - listWorkGroups(): Promise; searchWorkGroups(query: { search: string; offset: number; limit: number }): Promise>; getWorkGroup(id: number): Promise; @@ -311,7 +385,7 @@ export interface ApiClient { ): Promise; deleteWorkGroup(id: number): Promise; - // — API gateways — + // - API gateways - listApiGateways(): Promise; searchApiGateways(query: { search: string; @@ -320,15 +394,23 @@ export interface ApiClient { limit: number; }): Promise>; getApiGateway(id: number): Promise; + /** Calls to the gateway turned away, and why: kept by the node that answers, since it started. */ + getGatewayRejections(id: number): Promise; searchGatewayAttachments( apiGatewayId: number, query: { search: string; offset: number; limit: number }, ): Promise>; createApiGateway(input: { name: string; urlName: string }): Promise; - /** Leave `authentication` out to keep what the gateway has — pausing it must not change who can call. */ + /** Leave `authentication` out to keep what the gateway has - pausing it must not change who can call. */ updateApiGateway( id: number, - changes: { name: string; urlName: string; inactive: boolean; authentication?: GatewayAuthentication }, + changes: { + name: string; + urlName: string; + inactive: boolean; + authentication?: GatewayAuthentication; + failureAnswer?: GatewayFailureAnswer; + }, ): Promise; deleteApiGateway(id: number): Promise; /** The subscription is either an existing id or defined inline; the endpoint commits both as one. */ @@ -336,7 +418,7 @@ export interface ApiClient { updateGatewayAttachment(id: number, input: { partnerId: number; subscriptionId: number }): Promise; removeGatewayAttachment(id: number, partnerId: number): Promise; - // — bus gateways — + // - bus gateways - listBusGateways(): Promise; searchBusGateways(query: { search: string; @@ -359,7 +441,7 @@ export interface ApiClient { ): Promise; deleteBusGateway(id: number): Promise; - // ——— Data sources ——— + // --- Data sources --- /** What Bitween can connect to, and what each provider accepts. */ listDataSourceProviders(): Promise; listDataSources(): Promise; @@ -374,7 +456,7 @@ export interface ApiClient { adapterId: string; properties: Record; secretProperties: string[]; - /** Broker, Relational, Document, ObjectStore or Http — the provider declares it. */ + /** Broker, Relational, Document, ObjectStore or Http - the provider declares it. */ kind?: string; }): Promise<{ id: number }>; updateDataSource( @@ -386,6 +468,7 @@ export interface ApiClient { properties: Record; secretProperties: string[]; inactive: boolean; + placement: string; deduplicationWindowDays: number; softMemoryLimitMb: number; hardMemoryLimitMb: number; @@ -404,7 +487,7 @@ export interface ApiClient { args?: Record, ): Promise; - // ——— data source statements: the SQL a relational data source may run ——— + // --- data source statements: the SQL a relational data source may run --- listDataSourceStatements(dataSourceId: number): Promise; createDataSourceStatement( dataSourceId: number, @@ -413,7 +496,7 @@ export interface ApiClient { sql: string; description?: string | null; workGroupId?: number | null; - /** Only for a statement a receiver polls with — see DataSourceStatement. */ + /** Only for a statement a receiver polls with - see DataSourceStatement. */ cursorColumn?: string | null; keyColumn?: string | null; }, @@ -441,7 +524,7 @@ export interface ApiClient { ): Promise; removeBusRoute(id: number, routeId: number): Promise; - // — retry policies — + // - retry policies - listRetryPolicies(): Promise; searchRetryPolicies(query: { search: string; @@ -472,24 +555,34 @@ export interface ApiClient { getRetryUsage(policyId: number): Promise; /** * The same report for one subscription, which is the only way to reach one whose policy is an - * inline `CustomRetryPolicy` — those carry no policy id for the policy-scoped report to address, + * inline `CustomRetryPolicy` - those carry no policy id for the policy-scoped report to address, * yet still spend budget and can sit stopped with no counter anyone can see. */ getSubscriptionRetryUsage(subscriptionId: number): Promise; - /** The failures one group caught for one subscription — what its spent budget went on. */ + /** The failures one group caught for one subscription - what its spent budget went on. */ getRetryAttempts(policyId: number, pair: { subscriptionId: number; groupId: string }): Promise; /** Hands a spent budget back so the group retries again. Omit a field to reset across it. */ resetRetryUsage(policyId: number, pair?: { subscriptionId?: number; groupId?: string }): Promise; /** Reset by subscription, for the inline-policy case the policy-scoped reset cannot reach. */ resetSubscriptionRetryUsage(subscriptionId: number, groupId?: string): Promise; - /** Sets, changes or clears where one pair's alert goes — the most specific level. */ + /** Sets, changes or clears where one pair's alert goes - the most specific level. */ saveRetryAlertOverride( policyId: number, input: { subscriptionId: number; groupId: string } & RetryAlertConfig, ): Promise; - // — settings — + // - settings - listSettings(): Promise; + getAbout(): Promise; + + // - subscription categories - + listCategories(): Promise; + createCategory(input: { code: string; description: string }): Promise; + updateCategory(id: number, input: { code: string; description: string }): Promise; + /** Refused while any subscription is filed under it. */ + deleteCategory(id: number): Promise; + getBackgroundWork(): Promise; + getClusterNodes(): Promise; /** How long exchanges and their files are kept under the saved settings. `refresh` re-reads the bucket's rules. */ getRetention(refresh?: boolean): Promise; /** The same, for settings not saved yet. Changes nothing. */ @@ -499,19 +592,20 @@ export interface ApiClient { /** `value: null` resets the setting back to its default. */ updateSetting(key: string, value: string | null): Promise; - // — notifiers — - // No backend delete/test-send endpoint exists yet (BACKEND_WIRING_PLAN.md G8) — hidden in the UI. - // Channel choices come from listAdapters("handler") — same catalog as any other handler slot. + // - notifiers - + // No backend delete/test-send endpoint exists yet (BACKEND_WIRING_PLAN.md G8) - hidden in the UI. + // Channel choices come from listAdapters("handler") - same catalog as any other handler slot. searchNotifiers(query: { search: string; offset: number; limit: number }): Promise>; getNotifier(id: number): Promise; createNotifier(input: { name: string }): Promise; updateNotifier(id: number, changes: Omit): Promise; deleteNotifier(id: number): Promise; - // — exchanges — + // - exchanges - searchExchanges(query: ExchangeQuery): Promise>; + getExchangeBreakdown(query: ExchangeQuery): Promise; - // — audit trail — + // - audit trail - /** Who changed what, and when. Filters narrow it; with none set it is the whole trail. */ searchAudit(query: AuditQuery): Promise>; /** Fetches a stage document's raw text content by its storage key (`ExchangeFileRef.key`). */ @@ -539,20 +633,34 @@ export interface ApiClient { data: string; }): Promise<{ id: string }>; - // — scheduled retries — + // - scheduled retries - searchScheduledRetries(query: ScheduledRetryQuery): Promise>; /** Executes a pending auto-retry immediately instead of waiting for its slot. */ runScheduledRetryNow(id: string): Promise; - // — queue health — + // - queue health - getQueueHealth(): Promise; /** Deletes unattended lanes (all of each one's queues) and whatever they hold. */ deleteUnattendedQueues(queueNames: string[]): Promise; + /** The oldest messages in a dead-letter queue, left where they are. */ + browseDeadLetters(queueName: string, count?: number): Promise; + /** Sends a dead-letter queue's messages back to be tried again; resolves to how many went. */ + requeueDeadLetters(queueName: string): Promise; - // — dashboard — + // - dashboard - getDashboard(): Promise; - // — mappers — + // Metrics, read from the hourly rows the server keeps. + getMetricsOverview(): Promise; + getAdapterUsage(days: number): Promise; + getAdapterUsageDetail(id: string, days: number): Promise; + getSubscriptionHealth(days: number): Promise; + getSubscriptionMetrics(id: number, days: number): Promise; + getPartnerUsage(days: number): Promise; + getNodeMetrics(): Promise; + getInsights(): Promise; + + // - mappers - /** Executes a Scriban template against sample input, injecting the partner's adapter properties and global value sets exactly as the runtime mapper does. */ previewMapping(input: { scribanTemplate: string; @@ -562,7 +670,7 @@ export interface ApiClient { /** * Maps a sample document with unsaved rules, through the same read/map/write the exchange - * pipeline runs — so what the editor shows is what a partner would receive. + * pipeline runs - so what the editor shows is what a partner would receive. * * `ruleErrors` carries one entry per rule that could not be applied, keyed by its target path, * so the editor can mark the rows that are wrong. `error` is set instead when the mapping could diff --git a/SW.Bitween.Web/ClientApp/src/api/http/__tests__/errors.test.tsx b/SW.Bitween.Web/ClientApp/src/api/http/__tests__/errors.test.tsx new file mode 100644 index 00000000..c8c71805 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/__tests__/errors.test.tsx @@ -0,0 +1,34 @@ +import { http, HttpResponse } from "msw"; +import { describe, expect, it } from "vitest"; +import { server } from "../../../__tests__/support/server"; +import { apiPath } from "../../../__tests__/support/renderApp"; +import { ApiRequestError, shouldRetryQuery } from "../../types"; +import { post } from "../request"; + +/** How a refusal from the API reaches the screen: what it says, and whether it is asked again. */ +describe("errors from the API", () => { + it("keeps every validation message, not only the first", async () => { + server.use( + http.post(apiPath("/things"), () => + HttpResponse.json({ Name: ["A name is needed."], Port: ["The port must be a number."] }, { status: 400 }), + ), + ); + + const error = await post("/things", {}).catch((e: unknown) => e); + + expect(error).toBeInstanceOf(ApiRequestError); + const refusal = error as ApiRequestError; + expect(refusal.message).toBe("A name is needed. The port must be a number."); + expect(refusal.code).toBe("Name"); + expect(refusal.status).toBe(400); + expect(refusal.errors).toEqual({ Name: ["A name is needed."], Port: ["The port must be a number."] }); + }); + + it("doesn't ask again for a refusal, but does for a server error or a dropped connection", () => { + expect(shouldRetryQuery(0, new ApiRequestError("NOT_FOUND", "gone", 404))).toBe(false); + expect(shouldRetryQuery(0, new ApiRequestError("HTTP_403", "no", 403))).toBe(false); + expect(shouldRetryQuery(0, new ApiRequestError("ERROR", "boom", 500))).toBe(true); + expect(shouldRetryQuery(0, new TypeError("Failed to fetch"))).toBe(true); + expect(shouldRetryQuery(3, new ApiRequestError("ERROR", "boom", 500))).toBe(false); + }); +}); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/adapterDrafts.ts b/SW.Bitween.Web/ClientApp/src/api/http/adapterDrafts.ts new file mode 100644 index 00000000..f363bcd2 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/adapterDrafts.ts @@ -0,0 +1,43 @@ +import type { ApiClient } from "../client"; +import type { AdapterDraft, AdapterDraftSummary, DraftBuild, DraftPublishResult, DraftRun, AdapterUploadResult } from "../types"; +import { get, post, request } from "./request"; + +export const adapterDraftMethods = { + listAdapterDrafts: () => get("/adapterdrafts"), + + getAdapterDraft: (id: number) => get(`/adapterdrafts/${id}`), + + createAdapterDraft: (input) => post("/adapterdrafts", input), + + draftFromVersion: (adapterId: string, version: string) => + post("/adapterdrafts", { fromAdapterId: adapterId, fromVersion: version }), + + saveAdapterDraft: (id: number, files: Record, baseHash?: string) => + post(`/adapterdrafts/${id}`, baseHash ? { files, baseHash } : { files }), + + async deleteAdapterDraft(id: number) { + await request(`/adapterdrafts/${id}`, { method: "DELETE" }); + }, + + buildAdapterDraft: (id: number, settings: Record, buildOnly = false) => + post(`/adapterdrafts/${id}/build`, { settings, buildOnly }), + + tryAdapterDraft: (id: number, settings: Record, command: string, input: string) => + post(`/adapterdrafts/${id}/try`, { settings, command, input }), + + publishAdapterDraft: (id: number, input) => post(`/adapterdrafts/${id}/publish`, input), + + async promoteAdapter(adapterId: string, version: string) { + await post("/adapters/promote", { adapterId, version }); + }, + + async withdrawAdapterVersion(adapterId: string, version: string) { + await post("/adapters/withdraw", { adapterId, version }); + }, + + uploadAdapterPackage(file: Blob, options: { version: string; current: boolean; releaseNotes: string }) { + const query = new URLSearchParams({ version: options.version, current: String(options.current) }); + if (options.releaseNotes.trim()) query.set("releaseNotes", options.releaseNotes.trim()); + return request(`/adapters/packages?${query.toString()}`, { method: "POST", file }); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts b/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts index 9d8259c1..acd692fc 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/adapters.ts @@ -1,5 +1,5 @@ import type { ApiClient } from "../client"; -import type { AdapterInfo, AdapterKind, AdapterProp } from "../types"; +import type { AdapterInfo, AdapterKind, AdapterProp, AdapterSourceFile, AdapterSourceListing } from "../types"; import { get } from "./request"; interface RawStartupValue { @@ -13,6 +13,23 @@ interface RawCatalogAdapter { native: boolean; versions: string[] | null; startupValues: Record | null; + // From the catalog and manifest - absent from servers that predate them. + currentVersion?: string | null; + versionHistory?: { + version: string; + publishedOn?: string | null; + publishedBy?: string | null; + releaseNotes?: string | null; + withdrawn?: boolean; + hasSource?: boolean; + runtime?: string | null; + }[] | null; + displayName?: string | null; + summary?: string | null; + description?: string | null; + publisher?: string | null; + icon?: string | null; + tags?: string[] | null; } // The backend's Prefix param takes the plural, lowercase form. @@ -27,10 +44,10 @@ const KIND_PREFIX: Record = { * What to call an adapter on screen. * * An adapter's id is its C# class name, so every in-process one is spelled - * `NativeSomething` — and with no friendly name coming from the backend, that + * `NativeSomething` - and with no friendly name coming from the backend, that * prefix was what people read in every picker. It says nothing a user acts on: * where an adapter runs is the *other* list's business now, not part of its name. - * Stripped for display only — `id` is untouched, and stays what a subscription + * Stripped for display only - `id` is untouched, and stays what a subscription * stores, what the row is searchable by, and what is shown when the catalog * doesn't know an adapter. */ @@ -62,10 +79,36 @@ export const adapterMethods = { return (rows ?? []).map((r) => ({ id: r.key, kind, - label: displayName(r.key), + // What the adapter calls itself, when its manifest says; otherwise worked out from its id. + label: r.displayName?.trim() || displayName(r.key), native: r.native, versions: r.versions ?? [], props: toProps(r.startupValues), + summary: r.summary ?? undefined, + description: r.description ?? undefined, + publisher: r.publisher ?? undefined, + icon: r.icon ?? undefined, + tags: r.tags ?? [], + currentVersion: r.currentVersion ?? null, + versionHistory: (r.versionHistory ?? []).map((v) => ({ + version: v.version, + publishedOn: v.publishedOn ?? null, + publishedBy: v.publishedBy ?? null, + releaseNotes: v.releaseNotes ?? null, + withdrawn: v.withdrawn ?? false, + hasSource: v.hasSource ?? false, + runtime: v.runtime ?? null, + })), })); }, + + getAdapterSource(adapterId: string, version: string): Promise { + const q = new URLSearchParams({ adapterId, version }); + return get(`/adapters/source?${q}`); + }, + + getAdapterSourceFile(adapterId: string, version: string, path: string): Promise { + const q = new URLSearchParams({ adapterId, version, path }); + return get(`/adapters/sourcefile?${q}`); + }, } satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts b/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts index 65216886..23019350 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/appConfig.ts @@ -10,7 +10,7 @@ export interface AppConfig { isRabbitMqManagementConfigured?: boolean; /** Effective brand values (any stored override already applied), keyed like `ThemeOptions`. */ theme?: Record; - /** The same keys as configured, before any override — lets us tell "set" from "untouched". */ + /** The same keys as configured, before any override - lets us tell "set" from "untouched". */ themeDefaults?: Record; } @@ -18,7 +18,7 @@ let cached: Promise | null = null; /** * Bootstrap config served before authentication (MSAL parameters, feature - * flags, branding). Fetched once and memoised. Never throws to callers — an + * flags, branding). Fetched once and memoised. Never throws to callers - an * unreachable backend just yields an empty config, so the login page degrades * gracefully. */ diff --git a/SW.Bitween.Web/ClientApp/src/api/http/categories.ts b/SW.Bitween.Web/ClientApp/src/api/http/categories.ts new file mode 100644 index 00000000..601ec202 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/categories.ts @@ -0,0 +1,20 @@ +import type { ApiClient } from "../client"; +import type { SubscriptionCategory } from "../types"; +import { get, post } from "./request"; + +/** Every category there is: a short list, read whole. */ +const ALL = 500; + +export const categoryMethods = { + async listCategories(): Promise { + const res = await get<{ result: SubscriptionCategory[] | null }>(`/subscriptioncategories?limit=${ALL}`); + return (res.result ?? []).sort((a, b) => a.code.localeCompare(b.code)); + }, + createCategory: (input: { code: string; description: string }) => post("/subscriptioncategories", input), + async updateCategory(id: number, input: { code: string; description: string }) { + await post(`/subscriptioncategories/${id}`, input); + }, + async deleteCategory(id: number) { + await post(`/subscriptioncategories/${id}/delete`, {}); + }, +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/cliSignIn.ts b/SW.Bitween.Web/ClientApp/src/api/http/cliSignIn.ts new file mode 100644 index 00000000..46f3676f --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/cliSignIn.ts @@ -0,0 +1,8 @@ +import { post } from "./request"; + +/** + * Signs the bitween CLI in as the member signed in here: a one-time code for the CLI's PKCE + * challenge, which only the CLI that made the challenge can redeem. + */ +export const grantCliSignIn = (codeChallenge: string) => + post<{ code: string }>("/accounts/cligrant", { codeChallenge }); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts index 4bd77677..9f7be4a0 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts @@ -3,7 +3,7 @@ import type { DashboardData, ExchangeStatus } from "../types"; import { subscriptionMethods } from "./subscriptions"; import { get } from "./request"; -// ——— backend shapes (camelCase over the wire) ——— +// --- backend shapes (camelCase over the wire) --- interface SearchyResponse { result: T[]; totalCount: number; @@ -33,92 +33,51 @@ interface RawRetrySummary { }[]; } -const isBad = (raw: Pick) => - raw.status === false || (raw.status === true && raw.responseBad === true); -const isProcessing = (raw: Pick) => raw.status === null; const toStatus = (raw: Pick): ExchangeStatus => raw.status === null ? "processing" : !raw.status ? "failed" : raw.responseBad ? "badResponse" : "success"; +const LATEST = 6; + export const dashboardMethods = { + /** + * What the dashboard shows beside the metrics (which come from /metrics/overview): the latest + * failures, what is in flight, retries, queue alerts and subscriptions needing attention. + * + * Traffic, rates and the busiest used to be counted here, in the browser, from up to 1000 + * exchanges fetched every minute: wrong at any volume above that, and a heavy query for every + * open dashboard. They come from the server's hourly metrics now; what is left here is small, + * bounded queries over the last two weeks. + */ async getDashboard(): Promise { - const dayMs = 86_400_000; - const startOfTodayUtc = new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime(); - // 14 calendar days including today. - const windowStart = startOfTodayUtc - 13 * dayMs; - - // One bulk fetch covers every stat that's derived from exchange rows - // (today/yesterday/successRate7d/trafficByDay/busiest/latestFailures) — - // far fewer round trips than counting each bucket with its own filtered - // request, and exact rather than approximated. Bounded to a generous page - // size for what's a modest-scale ops tool; a very high-volume deployment - // would need real pagination here. - const [xchangeRes, delayedRes, failuresRes, retrySummary, alertsRaw, subscriptionRows] = await Promise.all([ + const now = Date.now(); + const since = (ms: number) => encodeURIComponent(`StartedOn:6:${new Date(now - ms).toISOString()}`); + const twoWeeks = since(14 * 86_400_000); + const latest = (status: number) => get>( - `/xchanges?filter=${encodeURIComponent(`StartedOn:6:${new Date(windowStart).toISOString()}`)}&size=1000&sort=StartedOn:1`, - ), - get>("/delayedretries?size=1"), - // Counted by the search rather than from the rows above, for two reasons: those are capped - // at a page of 1000 and at 14 days, and this number has to agree with the list the tile - // opens — which carries no date bound. Only the total is wanted, so no rows are fetched. - get>( - `/xchanges?filter=${encodeURIComponent("StatusFilter:1:3")}&filter=${encodeURIComponent("LatestOnly:1:true")}&size=1`, - ), - get("/dashboard/retrysummary"), - // Depends on RabbitMQ management being configured on the backend — don't let it take the - // rest of the dashboard down when it isn't; the "Queue alerts" tile flags it instead. - get("/ops/alerts").catch(() => null), - subscriptionMethods.listSubscriptionRows(), - ]); - - const rows = xchangeRes.result; - const startedAt = (r: RawXchangeForDashboard) => Date.parse(r.startedOn); - const subscriptionNameById = new Map(subscriptionRows.map((i) => [i.id, i.name])); + `/xchanges?filter=${twoWeeks}&filter=${encodeURIComponent(`StatusFilter:1:${status}`)}&sort=StartedOn:2&size=${LATEST}`, + ); - const todayRows = rows.filter((r) => startedAt(r) >= startOfTodayUtc); - const yesterdayRows = rows.filter((r) => { - const t = startedAt(r); - return t >= startOfTodayUtc - dayMs && t < startOfTodayUtc; - }); - // The last 7 calendar days including today — the same boundary as the - // final 7 entries of trafficByDay below. - const sevenDaysAgo = startOfTodayUtc - 6 * dayMs; - const week = rows.filter((r) => startedAt(r) >= sevenDaysAgo); - const finished7d = week.filter((r) => !isProcessing(r)); - const successRate7d = - finished7d.length === 0 - ? 100 - : Math.round((finished7d.filter((r) => !isBad(r)).length / finished7d.length) * 100); + const [failed, badResponses, inFlight, delayedRes, failuresRes, retrySummary, alertsRaw, subscriptionRows] = + await Promise.all([ + latest(3), + latest(2), + get>(`/xchanges?filter=${since(86_400_000)}&filter=${encodeURIComponent("StatusFilter:1:0")}&size=1`), + get>("/delayedretries?size=1"), + // Only the total is wanted: the problems still to deal with, which the tile's list opens. + get>( + `/xchanges?filter=${encodeURIComponent("StatusFilter:1:3")}&filter=${encodeURIComponent("LatestOnly:1:true")}&size=1`, + ), + get("/dashboard/retrysummary"), + // Depends on RabbitMQ management being configured on the backend - don't let it take the + // rest of the dashboard down when it isn't; the "Queue alerts" tile flags it instead. + get("/ops/alerts").catch(() => null), + subscriptionMethods.listSubscriptionRows(), + ]); - const trafficByDay = Array.from({ length: 14 }, (_, i) => { - const dayStart = windowStart + i * dayMs; - const dayRows = rows.filter((r) => { - const t = startedAt(r); - return t >= dayStart && t < dayStart + dayMs; - }); - return { - date: new Date(dayStart).toISOString(), - success: dayRows.filter((r) => !isBad(r)).length, - failed: dayRows.filter(isBad).length, - }; - }); - - const bySubscription = new Map(); - for (const r of week) { - if (r.subscriptionId === null) continue; - const entry = bySubscription.get(r.subscriptionId) ?? { count: 0, failed: 0 }; - entry.count++; - if (isBad(r)) entry.failed++; - bySubscription.set(r.subscriptionId, entry); - } - const busiest = [...bySubscription.entries()] - .map(([id, v]) => ({ id, name: subscriptionNameById.get(id) ?? `#${id}`, ...v })) - .sort((a, b) => b.count - a.count) - .slice(0, 5); - - const latestFailures = rows - .filter(isBad) + const subscriptionNameById = new Map(subscriptionRows.map((i) => [i.id, i.name])); + const latestFailures = [...failed.result, ...badResponses.result] .sort((a, b) => b.startedOn.localeCompare(a.startedOn)) - .slice(0, 6) + .slice(0, LATEST) .map((r) => ({ id: r.id, status: toStatus(r), @@ -130,20 +89,12 @@ export const dashboardMethods = { })); return { - today: { - total: todayRows.length, - failed: todayRows.filter(isBad).length, - processing: todayRows.filter(isProcessing).length, - }, - yesterdayTotal: yesterdayRows.length, + inFlight: inFlight.totalCount, failuresToActOn: failuresRes.totalCount, failingChains: retrySummary.failingChains ?? [], retriesLast7Days: retrySummary.retriesLast7Days ?? { finished: 0, succeeded: 0 }, - successRate7d, pendingRetries: delayedRes.totalCount, queueAlerts: alertsRaw === null ? null : alertsRaw.filter((a) => a.severity !== "Info").length, - trafficByDay, - busiest, latestFailures, attention: { failingSubscriptions: subscriptionRows diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dataSourceStatements.ts b/SW.Bitween.Web/ClientApp/src/api/http/dataSourceStatements.ts index 7cf52b0c..a530311d 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/dataSourceStatements.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/dataSourceStatements.ts @@ -9,7 +9,7 @@ interface SearchyResponse { } /** - * What a save answers with. `checked` is false when the database was never asked — the adapter for + * What a save answers with. `checked` is false when the database was never asked - the adapter for * this connection is not running on the node that handled the request, so there was nobody to * validate against. Worth saying: a save that reported nothing would look exactly like one that * had been verified. @@ -57,7 +57,7 @@ const toStatement = (raw: RawStatement): DataSourceStatement => ({ const EVERYTHING = 1_000_000; -export const dataSourceStatementMethods: Partial = { +export const dataSourceStatementMethods = { async listDataSourceStatements(dataSourceId: number): Promise { // Filtered server-side by data source: a statement is only ever meaningful next to the // connection it runs against, and no screen wants all of them at once. Rule 1 is EqualsTo. @@ -129,4 +129,4 @@ export const dataSourceStatementMethods: Partial = { const raw = await post(`/datasourcestatements/${id}/usage`, {}); return { ...raw, usedBy: raw.usedBy ?? [] }; }, -}; +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dataSources.ts b/SW.Bitween.Web/ClientApp/src/api/http/dataSources.ts index 7641c41b..9c30ac92 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/dataSources.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/dataSources.ts @@ -34,7 +34,7 @@ interface RawDataSource { lastException: string | null; consecutiveFailures: number; ownedByNode: string | null; - // Only the detail endpoint returns these — the list deliberately carries no connection + // Only the detail endpoint returns these - the list deliberately carries no connection // settings at all, because even masked they are exposure with no purpose in a table. properties?: Record | null; secretProperties?: string[] | null; @@ -79,7 +79,7 @@ const toDetail = (raw: RawDataSource): DataSourceDetail => ({ }); // The backend's own default only applies when the caller omits the parameter, so asking for -// "everything" means naming a generously large number — same as the work groups module. +// "everything" means naming a generously large number - same as the work groups module. const EVERYTHING = 1_000_000; export const dataSourceMethods = { @@ -147,6 +147,7 @@ export const dataSourceMethods = { properties: Record; secretProperties: string[]; inactive: boolean; + placement: string; deduplicationWindowDays: number; softMemoryLimitMb: number; hardMemoryLimitMb: number; @@ -172,7 +173,7 @@ export const dataSourceMethods = { }); }, - /** Live, from the heartbeat. Scoped to the node that answers — see the backend handler. */ + /** Live, from the heartbeat. Scoped to the node that answers - see the backend handler. */ async getDataSourceTelemetry(id: number): Promise { const raw = await get(`/datasources/${id}/telemetry`); return { ...raw, details: raw.details ?? {}, commands: raw.commands ?? [] }; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts index d2db2827..a219f97a 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts @@ -9,7 +9,7 @@ import type { } from "../types"; import { exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; -import { get, post, request } from "./request"; +import { enrichment, get, post, request } from "./request"; import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; interface SearchyResponse { @@ -20,7 +20,7 @@ interface RawKeyAndValue { key: string; value: string; } -/** Seeded by the model — mirrors Document.AggregationDocumentId on the backend. */ +/** Seeded by the model - mirrors Document.AggregationDocumentId on the backend. */ const AGGREGATION_DOCUMENT_ID = 10001; interface RawDocument { @@ -33,7 +33,8 @@ interface RawDocument { duplicateInterval: number; disregardsUnfilteredMessages: boolean; promotedProperties: RawKeyAndValue[] | null; - /** Counted by the backend — see DocumentRow.UsedByCount. */ + validationSchema?: string | null; + /** Counted by the backend - see DocumentRow.UsedByCount. */ usedByCount: number; retiredOn: string | null; } @@ -83,17 +84,23 @@ const toInformationType = (d: RawDocument): InformationType => ({ duplicateIntervalMinutes: d.duplicateInterval, disregardsUnfilteredMessages: d.disregardsUnfilteredMessages, promotedProperties: (d.promotedProperties ?? []).map((p) => ({ key: p.key, path: p.value })), + validationSchema: d.validationSchema ?? null, createdOn: "", retiredOn: d.retiredOn ?? null, isSystem: d.id === AGGREGATION_DOCUMENT_ID, }); async function fetchDetail(id: number): Promise { + // Only the type itself is required. The rest come from areas a role that can see information + // types may not be allowed into, and without them the page still has everything it edits. const [d, subs, busGateways, recentExchanges] = await Promise.all([ get(`/documents/${id}`), - fetchSubscriptionsByDocument(id), - gatewayMethods.listBusGateways(), - exchangeMethods.searchExchanges({ informationTypeId: id, offset: 0, limit: 8 }), + enrichment(fetchSubscriptionsByDocument(id), []), + enrichment(gatewayMethods.listBusGateways(), []), + enrichment(exchangeMethods.searchExchanges({ informationTypeId: id, offset: 0, limit: 8 }), { + result: [], + total: 0, + }), ]); return { ...toInformationType(d), @@ -123,6 +130,8 @@ const documentBody = (t: Omit) => ({ duplicateInterval: t.duplicateIntervalMinutes, disregardsUnfilteredMessages: t.disregardsUnfilteredMessages, promotedProperties: t.promotedProperties.map((p) => ({ key: p.key, value: p.path })), + // Empty removes it; the server reads a missing one as "leave it alone". + validationSchema: t.validationSchema ?? "", }); export const documentMethods = { diff --git a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts index 9bd99711..baa57549 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts @@ -2,6 +2,7 @@ import type { ApiClient } from "../client"; import type { BulkRetryPlan, BulkRetrySelection, + ExchangeBreakdown, ExchangeQuery, ExchangeRow, ExchangeStatus, @@ -15,7 +16,7 @@ import { partnerMethods } from "./partners"; import { download, get, post } from "./request"; import { searchyQueryString } from "./searchQuery"; -// ——— backend shapes (camelCase over the wire) ——— +// --- backend shapes (camelCase over the wire) --- interface SearchyResponse { result: T[]; totalCount: number; @@ -97,7 +98,7 @@ interface RawDelayedRetryRow { } /** - * The backend has no real ExchangeStatus enum — it's derived from two + * The backend has no real ExchangeStatus enum - it's derived from two * nullable booleans. `status == null` while still running; once set, a bad * *response* (the handler delivered but the receiver answered with an error) * is distinct from an outright failure. @@ -167,13 +168,13 @@ const toScheduledRetryRow = (raw: RawDelayedRetryRow): ScheduledRetryRow => ({ * The backend's date-`Range` filter (rule 21) is unconditionally broken: its * shared parser (`SearchyFilter.cs`, SW-PrimitiveTypes) does `DateTime.Parse` * with no `DateTimeStyles`, which can only produce `Local` or `Unspecified` - * `Kind` — Npgsql then refuses to bind it to a `timestamptz` column and the + * `Kind` - Npgsql then refuses to bind it to a `timestamptz` column and the * whole search 500s, regardless of what the client sends. Two scalar * comparisons (`GreaterThanOrEquals`/`LessThanOrEquals`, rules 6/8) go - * through a different code path and work correctly — use those instead. + * through a different code path and work correctly - use those instead. */ /** - * Just the filters, without paging — what "select all matching" sends, so a bulk retry acts on + * Just the filters, without paging - what "select all matching" sends, so a bulk retry acts on * the same set the list was showing rather than on the 25 rows that happened to be on screen. */ function buildExchangeFilters(query: ExchangeQuery): URLSearchParams { @@ -187,7 +188,7 @@ function buildExchangeFilters(query: ExchangeQuery): URLSearchParams { params.append("filter", `Id:4:text|${ids.join("|")}`); } // The server turns this into the same Id filter above, from the run's own record of what it - // created — so a run of any size is one short parameter rather than every id in the URL. + // created - so a run of any size is one short parameter rather than every id in the URL. if (query.receiveAttemptId !== undefined) params.append("filter", `ReceiveAttemptId:1:${query.receiveAttemptId}`); if (query.correlationId?.trim()) params.append("filter", `CorrelationId:1:${query.correlationId.trim()}`); @@ -197,6 +198,10 @@ function buildExchangeFilters(query: ExchangeQuery): URLSearchParams { if (query.propertyKey) params.append("filter", `PromotedPropertyKey:1:${query.propertyKey}`); const propertyValue = query.property?.trim(); if (propertyValue) params.append("filter", `PromotedPropertiesRaw:4:${propertyValue}`); + if (query.errorContains?.trim()) params.append("filter", `ErrorContains:4:${query.errorContains.trim()}`); + if (query.isRetry) params.append("filter", "IsRetry:1:true"); + if (query.scheduledRetry) params.append("filter", "HasScheduledRetry:1:true"); + if (query.aggregated) params.append("filter", "Aggregated:1:true"); if (query.from) params.append("filter", `StartedOn:6:${query.from}`); if (query.to) params.append("filter", `StartedOn:8:${query.to}`); return params; @@ -224,7 +229,7 @@ function buildScheduledRetryQuery(query: ScheduledRetryQuery): string { /** * A selection as the bulk endpoints take it. The filter goes over as the same query-string * fragment the list itself sends, percent-encoded the way the backend's parser expects (see - * `searchyQueryString`), and the backend re-runs it — the client never has to enumerate ids it + * `searchyQueryString`), and the backend re-runs it - the client never has to enumerate ids it * has not loaded. */ const selectionBody = (selection: BulkRetrySelection) => { @@ -278,9 +283,29 @@ export const exchangeMethods = { return { result: res.result.map((r) => toExchangeRow(r, partnerNameById)), total: res.totalCount }; }, + /** + * The status counts and volume over time behind the list's tabs and chart. The status is left + * out of the filter on purpose: the tabs are the choice between statuses, so each needs its + * count under every other filter. + */ + async getExchangeBreakdown(query: ExchangeQuery): Promise { + const params = buildExchangeFilters({ ...query, status: undefined }); + const res = await get(`/xchanges/summary${params.toString() ? `?${searchyQueryString(params)}` : ""}`); + return { + counts: { + processing: res.counts?.processing ?? 0, + success: res.counts?.success ?? 0, + badResponse: res.counts?.badResponse ?? 0, + failed: res.counts?.failed ?? 0, + }, + buckets: res.buckets ?? [], + bucketMinutes: res.bucketMinutes ?? 0, + }; + }, + async retryExchange(id: string, { reset }: { reset: boolean }): Promise<{ id: string }> { await post(`/xchanges/${id}/retry`, { reason: "Manual retry", reset }); - // Retry.cs returns null — look up the retry it just created (the newest + // Retry.cs returns null - look up the retry it just created (the newest // xchange with retryFor == id) for a real id to hand back to the caller. const res = await get>( `/xchanges?filter=${encodeURIComponent(`RetryFor:1:${id}`)}&sort=StartedOn:2&size=1`, @@ -290,7 +315,7 @@ export const exchangeMethods = { /** * Every attempt related to one exchange. Worth asking for only when the row says there is - * something to see — `retryFor` or `hasRetry` — since most exchanges have neither. + * something to see - `retryFor` or `hasRetry` - since most exchanges have neither. */ async getRetryTree(id: string): Promise { const raw = await get(`/xchanges/retrytree?id=${encodeURIComponent(id)}`); @@ -348,7 +373,7 @@ export const exchangeMethods = { documentId: input.target === "informationType" ? input.informationTypeId : null, data: input.data, }); - // Create.cs returns null too — look up the exchange it just created. When + // Create.cs returns null too - look up the exchange it just created. When // addressed at an information type, every matching subscription gets its // own exchange; we can only link to one, so take the newest. const res = await get>( diff --git a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts index 8fa0c5a2..69cc41f6 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts @@ -9,17 +9,19 @@ import type { BusGatewayRoute, BusGatewayRow, GatewayAuthentication, + GatewayFailureAnswer, GatewayAuthMethod, InlineSubscriptionDraft, MatchGroup, Paged, + GatewayRejections, } from "../types"; import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; import { inlineSubscriptionBody } from "./subscriptionBody"; import { get, post, request } from "./request"; import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; -// ——— backend shapes (camelCase over the wire) ——— +// --- backend shapes (camelCase over the wire) --- interface SearchyResponse { result: T[]; totalCount: number; @@ -45,7 +47,7 @@ interface RawApiGateway { partnersCount: number | null; inactive: boolean | null; // Search's list projection includes this too (backend change made alongside - // this batch) — but keep it optional since Create's bare POST response has none. + // this batch) - but keep it optional since Create's bare POST response has none. partners: RawApiGatewayPartner[] | null; // Only the detail endpoint sends these. authentication?: RawGatewayAuthentication | null; @@ -152,14 +154,14 @@ const toBusGatewayDetail = (raw: RawBusGateway): BusGatewayDetail => ({ ...toSource(raw), }); -/** The attachment always points at a subscription that already exists — a new one is +/** The attachment always points at a subscription that already exists - a new one is * created on its own page first, not inline here (unlike a bus gateway route, which - * still creates one in the same transaction — see `AddBusRouteInput`). */ + * still creates one in the same transaction - see `AddBusRouteInput`). */ export type AttachPartnerInput = { partnerId: number; subscriptionId: number }; /** * A route points at a subscription that already exists, or defines one. Exactly one, - * which the endpoint enforces — the union makes that unrepresentable rather than + * which the endpoint enforces - the union makes that unrepresentable rather than * merely wrong. */ export type AddBusRouteInput = { @@ -168,7 +170,7 @@ export type AddBusRouteInput = { } & ({ subscriptionId: number } | { newSubscription: InlineSubscriptionDraft }); export const gatewayMethods = { - // ——— API gateways ——— + // --- API gateways --- async listApiGateways(): Promise { const res = await get>("/apigateways"); @@ -193,12 +195,16 @@ export const gatewayMethods = { return { total: res.totalCount, result: (res.result ?? []).map(toApiGatewayRow) }; }, + getGatewayRejections(id: number): Promise { + return get(`/apigateways/${id}/rejections`); + }, + async getApiGateway(id: number): Promise { return toApiGatewayDetail(await get(`/apigateways/${id}`)); }, /** Paged, searched view of one gateway's attachments, for the gateway page's own - * table — `getApiGateway` keeps returning the full list, still needed by the + * table - `getApiGateway` keeps returning the full list, still needed by the * attach-partner picker's exclude list. */ async searchGatewayAttachments( apiGatewayId: number, @@ -221,9 +227,15 @@ export const gatewayMethods = { async updateApiGateway( id: number, - changes: { name: string; urlName: string; inactive: boolean; authentication?: GatewayAuthentication }, + changes: { + name: string; + urlName: string; + inactive: boolean; + authentication?: GatewayAuthentication; + failureAnswer?: GatewayFailureAnswer; + }, ): Promise { - // Update replaces the record, so every field it accepts has to be sent back — + // Update replaces the record, so every field it accepts has to be sent back - // omitting `inactive` would quietly reactivate a paused gateway on a rename. // Authentication is the exception: left out, the gateway keeps what it has. await post(`/apigateways/${id}`, { @@ -231,6 +243,8 @@ export const gatewayMethods = { urlName: changes.urlName, inactive: changes.inactive, ...(changes.authentication && { authentication: changes.authentication }), + // Left out, like authentication, the gateway keeps what it has. + ...(changes.failureAnswer && { failureAnswer: changes.failureAnswer }), }); return { id, name: changes.name, urlName: changes.urlName, inactive: changes.inactive, createdOn: "" }; }, @@ -249,7 +263,7 @@ export const gatewayMethods = { async updateGatewayAttachment(id: number, input: { partnerId: number; subscriptionId: number }): Promise { // Not a plain POST to updatepartner: ApiGatewayPartner's PK is the composite // (gatewayId, partnerId, subscriptionId), and the backend's UpdatePartner - // handler tries to mutate subscriptionId in place on a tracked entity — EF + // handler tries to mutate subscriptionId in place on a tracked entity - EF // Core rejects changes to a key column. Remove-then-add sidesteps it. await post(`/apigateways/${id}/removepartner`, { partnerId: input.partnerId }); await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, subscriptionId: input.subscriptionId }); @@ -259,7 +273,7 @@ export const gatewayMethods = { await post(`/apigateways/${id}/removepartner`, { partnerId }); }, - // ——— bus gateways ——— + // --- bus gateways --- async listBusGateways(): Promise { const res = await get>("/busgateways"); @@ -303,7 +317,7 @@ export const gatewayMethods = { inactive: false, }); // A gateway is created on the internal bus and moved onto a broker afterwards, on its own - // page — the source is a decision about an existing gateway, not a hurdle to creating one. + // page - the source is a decision about an existing gateway, not a hurdle to creating one. return { id, name, @@ -321,8 +335,8 @@ export const gatewayMethods = { id: number, changes: { name: string; inactive: boolean; dataSourceId?: number | null; endpoint?: string | null }, ): Promise { - // The bound information type is fixed at creation — Update.cs silently - // ignores documentId — but the request DTO still requires a value, so + // The bound information type is fixed at creation - Update.cs silently + // ignores documentId - but the request DTO still requires a value, so // fetch the current one to round-trip it rather than sending a bogus 0. const current = await get(`/busgateways/${id}`); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts index 82133a7a..f08053e2 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts @@ -1,8 +1,10 @@ import type { ApiClient } from "../client"; -import { NotWiredError } from "../types"; import { adapterMethods } from "./adapters"; +import { adapterDraftMethods } from "./adapterDrafts"; import { auditMethods } from "./audit"; +import { categoryMethods } from "./categories"; import { dashboardMethods } from "./dashboard"; +import { metricsMethods } from "./metrics"; import { dataSourceMethods } from "./dataSources"; import { dataSourceStatementMethods } from "./dataSourceStatements"; import { documentMethods } from "./documents"; @@ -17,16 +19,15 @@ import { queueHealthMethods } from "./queueHealth"; import { retryPolicyMethods } from "./retryPolicies"; import { sessionMethods } from "./session"; import { settingsMethods } from "./settings"; +import { serviceAccountMethods } from "./serviceAccounts"; import { teamMethods } from "./team"; import { workGroupMethods } from "./workGroups"; /** - * The single real client. Wired domains are merged in here; every other - * ApiClient method resolves to a rejected NotWiredError so its screen shows an - * honest "Not connected yet" state instead of fake data. Each batch adds its - * domain module to `wired` (see BACKEND_WIRING_PLAN §5–6). + * The client: every domain's methods merged. Typed as the whole ApiClient, so a method declared and + * not implemented is a compile error rather than a call that fails at run time. */ -const wired: Partial = { +export const httpClient: ApiClient = { ...sessionMethods, ...auditMethods, ...partnerMethods, @@ -35,23 +36,19 @@ const wired: Partial = { ...workGroupMethods, ...retryPolicyMethods, ...subscriptionMethods, + ...categoryMethods, ...adapterMethods, + ...adapterDraftMethods, ...gatewayMethods, ...exchangeMethods, ...queueHealthMethods, ...dataSourceStatementMethods, ...dashboardMethods, + ...metricsMethods, ...dataSourceMethods, ...mapperMethods, ...notifierMethods, ...teamMethods, + ...serviceAccountMethods, ...settingsMethods, }; - -export const httpClient: ApiClient = new Proxy(wired, { - get(target, prop, receiver) { - if (typeof prop !== "string" || prop in target) return Reflect.get(target, prop, receiver); - // Anything not yet wired: a callable that rejects clearly, so `await api.x()` fails honestly. - return () => Promise.reject(new NotWiredError(prop)); - }, -}) as ApiClient; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts b/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts index b05102b9..b9809017 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/matchExpression.ts @@ -3,7 +3,7 @@ import type { MatchGroup, MatchNode } from "../types"; /** * The backend's match tree is strictly binary (and/or each take exactly two * operands) and uses snake_case type discriminators, unlike the frontend's - * n-ary MatchGroup. Shared by Subscriptions and BusGatewayRoute — both use the + * n-ary MatchGroup. Shared by Subscriptions and BusGatewayRoute - both use the * exact same backend type (`IPropertyMatchSpecification`). */ export type RawMatchSpec = @@ -48,7 +48,7 @@ function toBackendNode(node: MatchNode): RawMatchSpec | null { return { type: node.op === "oneOf" ? "one_of" : "not_one_of", path: node.path, values: node.values }; } const parts = node.children.map(toBackendNode).filter((s): s is RawMatchSpec => s !== null); - if (parts.length === 0) return null; // empty group — matches everything, i.e. no constraint + if (parts.length === 0) return null; // empty group - matches everything, i.e. no constraint if (parts.length === 1) return parts[0]; return parts.reduceRight((right, left) => ({ type: node.op, left, right })); } diff --git a/SW.Bitween.Web/ClientApp/src/api/http/metrics.ts b/SW.Bitween.Web/ClientApp/src/api/http/metrics.ts new file mode 100644 index 00000000..289518a1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/metrics.ts @@ -0,0 +1,25 @@ +import type { ApiClient } from "../client"; +import type { + AdapterUsageDetail, + AdapterUsageList, + Insights, + MetricsOverview, + NodeMetrics, + PartnerUsageList, + SubscriptionHealthList, + SubscriptionMetrics, +} from "../metricsTypes"; +import { get } from "./request"; + +export const metricsMethods = { + getMetricsOverview: () => get("/metrics/overview"), + getAdapterUsage: (days: number) => get(`/metrics/adapters?days=${days}`), + getAdapterUsageDetail: (id: string, days: number) => + get(`/metrics/adapter?id=${encodeURIComponent(id)}&days=${days}`), + getSubscriptionHealth: (days: number) => get(`/metrics/subscriptions?days=${days}`), + getSubscriptionMetrics: (id: number, days: number) => + get(`/metrics/subscription?id=${id}&days=${days}`), + getPartnerUsage: (days: number) => get(`/metrics/partners?days=${days}`), + getNodeMetrics: () => get("/metrics/nodes"), + getInsights: () => get("/metrics/insights"), +} satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts index 91e749db..d05d0cbb 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts @@ -22,7 +22,7 @@ interface RawNotifier { runOnFailedResult: boolean; runOnSubscriptions: { id: number; name: string | null }[] | null; } -/** Shape of a search-endpoint row — lighter than `RawNotifier`, no adapter properties. */ +/** Shape of a search-endpoint row - lighter than `RawNotifier`, no adapter properties. */ interface RawNotifierRow { id: number; name: string; @@ -41,7 +41,7 @@ interface RawNotification { } // Empty-valued properties are dropped: an adapter property with no value means // "not set", and keeping it would make a freshly-cleared field compare unequal to -// stored data that never had the key — leaving the Save bar up after an undo. +// stored data that never had the key - leaving the Save bar up after an undo. const toRecord = (kvs: RawKeyAndValue[] | null): Record => Object.fromEntries((kvs ?? []).filter((kv) => kv.value !== "").map((kv) => [kv.key, kv.value])); const toKvArray = (record: Record): RawKeyAndValue[] => diff --git a/SW.Bitween.Web/ClientApp/src/api/http/partners.ts b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts index f2151393..42b068c5 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/partners.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts @@ -3,13 +3,13 @@ import { ApiRequestError, type Paged, type Partner, type PartnerDetail, type Par import { exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; import { matchSummary } from "../../lib/match"; -import { get, post, request } from "./request"; +import { enrichment, get, post, request } from "./request"; import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; // The built-in SYSTEM partner (Partner.SystemId) can't be renamed or deleted. const SYSTEM_PARTNER_ID = 1; -// ——— backend shapes (camelCase over the wire) ——— +// --- backend shapes (camelCase over the wire) --- interface SearchyResponse { result: T[]; totalCount: number; @@ -31,6 +31,8 @@ interface RawPartnerDetail { adapterProperties: Record | null; secretProperties: string[] | null; loginIdentity?: string | null; + /** By key name: when a gateway last let a call through with it. */ + keysLastUsedOn?: Record | null; } // GET masks keys as `...(hidden)`; recover the visible prefix. @@ -63,12 +65,12 @@ async function writePartner( ): Promise { const name = patch.name ?? d.name; // Secrets read back as SECRET_SENTINEL, and sending the sentinel means "keep what is - // stored" — so a write that changes only the name carries the masks through untouched + // stored" - so a write that changes only the name carries the masks through untouched // and the stored values are never overwritten with dots. const adapterProperties = patch.adapterProperties ?? d.adapterProperties ?? {}; const secretProperties = patch.secretProperties ?? d.secretProperties ?? []; - // Update copies every field it is sent, so leaving this out — as issuing or revoking a key - // does — would clear the identity and cut the partner off from every gateway taking tokens. + // Update copies every field it is sent, so leaving this out - as issuing or revoking a key + // does - would clear the identity and cut the partner off from every gateway taking tokens. const loginIdentity = patch.loginIdentity !== undefined ? patch.loginIdentity : (d.loginIdentity ?? null); const apiCredentials = mutate((d.apiCredentials ?? []).map((c) => ({ key: c.key, value: c.value }))); await post(`/partners/${id}`, { name, adapterProperties, secretProperties, loginIdentity, apiCredentials }); @@ -89,7 +91,7 @@ export const partnerMethods = { return (res.result ?? []).map((p) => ({ id: p.id, name: p.name, - // The list endpoint sends property names, never their values — they can be + // The list endpoint sends property names, never their values - they can be // secrets. Anything needing values must fetch the partner's detail. adapterProperties: {}, secretProperties: [], @@ -124,18 +126,20 @@ export const partnerMethods = { }; }, - /** Light single-field fetch for the mapper editor's test-partner selector — avoids getPartner's gateway/exchange lookups. */ + /** Light single-field fetch for the mapper editor's test-partner selector - avoids getPartner's gateway/exchange lookups. */ async getPartnerAdapterProperties(id: number): Promise> { const d = await requireDetail(id); return d.adapterProperties ?? {}; }, async getPartner(id: number): Promise { + // Only the partner itself is required. The rest come from areas a role that can see partners + // may not be allowed into, and without them the page still has everything it edits. const [d, apiGateways, busGateways, recentExchanges] = await Promise.all([ requireDetail(id), - gatewayMethods.listApiGateways(), - gatewayMethods.listBusGateways(), - exchangeMethods.searchExchanges({ partnerId: id, offset: 0, limit: 8 }), + enrichment(gatewayMethods.listApiGateways(), []), + enrichment(gatewayMethods.listBusGateways(), []), + enrichment(exchangeMethods.searchExchanges({ partnerId: id, offset: 0, limit: 8 }), { result: [], total: 0 }), ]); return { id, @@ -149,6 +153,7 @@ export const partnerMethods = { name: c.key, keyPrefix: keyPrefixOf(c.value), createdOn: "", + lastUsedOn: d.keysLastUsedOn?.[c.key] ?? null, })), apiGateways: apiGateways .filter((g) => g.attachments.some((a) => a.partnerId === id)) diff --git a/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts b/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts index 3096a5c0..dd4c9edb 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/queueHealth.ts @@ -1,5 +1,7 @@ import type { ApiClient } from "../client"; import type { + BackgroundWork, + DeadLetterMessage, QueueHealthSnapshot, QueueLane, QueueSeverity, @@ -8,7 +10,7 @@ import type { } from "../types"; import { get, post } from "./request"; -// ——— backend shapes (camelCase over the wire; severities are PascalCase strings) ——— +// --- backend shapes (camelCase over the wire; severities are PascalCase strings) --- type RawSeverity = "Info" | "Warning" | "Critical"; interface RawSummary { totalConsumers: number; @@ -68,6 +70,11 @@ interface RawAlert { const toSeverity = (s: RawSeverity): QueueSeverity => (s === "Critical" ? "critical" : s === "Warning" ? "warning" : "healthy"); export const queueHealthMethods = { + /** Bitween's own system jobs and its outbox to the broker. */ + getBackgroundWork(): Promise { + return get("/ops/background"); + }, + async getQueueHealth(): Promise { const [summary, consumers, retries, deadLetters, alerts, unattended] = await Promise.all([ get("/ops/summary"), @@ -138,7 +145,7 @@ export const queueHealthMethods = { })), unattended, // The frontend's alert severity is narrower ("warning"|"critical") than - // the backend's ("Info"|"Warning"|"Critical") — informational alerts + // the backend's ("Info"|"Warning"|"Critical") - informational alerts // aren't alerts from the UI's point of view, so drop them. alerts: alerts .filter((a) => a.severity !== "Info") @@ -155,4 +162,15 @@ export const queueHealthMethods = { deleteUnattendedQueues(queueNames: string[]): Promise { return post("/ops/deleteunattendedqueues", { queueNames }); }, + + browseDeadLetters(queueName: string, count = 20): Promise { + return get( + `/ops/deadlettermessages?queue=${encodeURIComponent(queueName)}&count=${count}`, + ); + }, + + async requeueDeadLetters(queueName: string): Promise { + const result = await post<{ requeued: number }>("/ops/requeuedeadletters", { queue: queueName }); + return result.requeued; + }, } satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/references.ts b/SW.Bitween.Web/ClientApp/src/api/http/references.ts index 094f4e2a..0fb1affc 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/references.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/references.ts @@ -10,7 +10,7 @@ * The resolver splits the globals token on the **first** "." only and puts no * character-class restriction on either half, so these patterns are equally * permissive rather than assuming a slug charset. It also matches the prefix - * and looks up ids and keys with `OrdinalIgnoreCase` — hence the `i` flag, and + * and looks up ids and keys with `OrdinalIgnoreCase` - hence the `i` flag, and * why callers must compare what comes back case-insensitively. */ const GLOBAL_TOKEN_RE = /\{\{globals\.([^.]+)\.([^}]+)\}\}/gi; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/request.ts b/SW.Bitween.Web/ClientApp/src/api/http/request.ts index f123823f..10bbd62e 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/request.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/request.ts @@ -6,11 +6,66 @@ import { ApiRequestError } from "../types"; */ export const API_BASE = "/api"; -/** The Jwt is kept in localStorage; the refresh token is an HttpOnly cookie JS never sees. */ +/** + * Where earlier versions kept the Jwt. Read once at load only to delete it, so a token left + * behind by an older build does not outlive the upgrade. + */ export const TOKEN_KEY = "access_token"; /** - * Told when a request proves the session is over — the Jwt was refused and no + * Set while this browser holds a session, cleared when it ends. Not a credential - it only tells + * a new tab or a reload that a refresh cookie is worth trying, so the sign-in page doesn't spend + * the sign-in rate limit probing for a session that was never there. + */ +export const SIGNED_IN_KEY = "bitween_signed_in"; + +/** + * The Jwt lives in this module's memory and nowhere else; the refresh token is an HttpOnly + * cookie JS never sees. localStorage was readable by any script that ever ran on the page, and + * a stolen Jwt works from anywhere until it expires. A reload starts with no Jwt and gets one + * from the refresh cookie (see `getSession`). + */ +let accessToken: string | null = null; + +type SessionMessage = { type: "token"; jwt: string } | { type: "signed-out" }; + +/** + * How tabs share what used to be shared through localStorage: a new Jwt, and the end of the + * session. Missing in very old browsers, where each tab simply refreshes on its own. + */ +const channel: BroadcastChannel | null = + typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel("bitween-session"); + +// Node (the test runner) keeps a process alive while a channel listens; browsers have no unref. +(channel as unknown as { unref?: () => void } | null)?.unref?.(); + +let signedOutElsewhereListener: (() => void) | null = null; + +/** Told when another tab of this origin ends the session. `SessionProvider` registers itself. */ +export const onSignedOutElsewhere = (listener: (() => void) | null): void => { + signedOutElsewhereListener = listener; +}; + +channel?.addEventListener("message", (event: MessageEvent) => { + if (event.data?.type === "token") accessToken = event.data.jwt; + else if (event.data?.type === "signed-out") { + accessToken = null; + signedOutElsewhereListener?.(); + } +}); + +const safeStorage = (action: () => void): void => { + try { + action(); + } catch { + /* storage can be unavailable (private mode, blocked site data); the hint is only an optimisation */ + } +}; + +safeStorage(() => localStorage.removeItem(TOKEN_KEY)); + +/** + * Told when a request proves the session is over - the Jwt was refused and no * refresh cookie was left to replace it. * * A callback rather than a hook because this is plain module code that cannot @@ -24,9 +79,28 @@ export const onSessionEnded = (listener: (() => void) | null): void => { sessionEndedListener = listener; }; -export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY); -export const setToken = (jwt: string): void => localStorage.setItem(TOKEN_KEY, jwt); -export const clearToken = (): void => localStorage.removeItem(TOKEN_KEY); +export const getToken = (): string | null => accessToken; + +export const setToken = (jwt: string): void => { + accessToken = jwt; + safeStorage(() => localStorage.setItem(SIGNED_IN_KEY, "1")); + channel?.postMessage({ type: "token", jwt } satisfies SessionMessage); +}; + +export const clearToken = (): void => { + accessToken = null; + safeStorage(() => localStorage.removeItem(SIGNED_IN_KEY)); + channel?.postMessage({ type: "signed-out" } satisfies SessionMessage); +}; + +/** Whether this browser held a session when it last looked - worth a silent refresh to find out. */ +export const mayHaveSession = (): boolean => { + try { + return localStorage.getItem(SIGNED_IN_KEY) === "1"; + } catch { + return true; + } +}; /** Backend serializes camelCase; login returns `{ jwt }`. */ const readJwt = (data: unknown): string | null => @@ -35,14 +109,22 @@ const readJwt = (data: unknown): string | null => let refreshInFlight: Promise | null = null; /** - * Silent refresh: POST /accounts/login with an empty body — the HttpOnly + * Silent refresh: POST /accounts/login with an empty body - the HttpOnly * refresh_token cookie alone re-issues a Jwt. Returns the new token, or null * when the cookie is missing/expired. Bypasses `request()` to avoid recursion, * and dedupes concurrent callers behind one in-flight promise. + * + * Serialised across tabs with a Web Lock. Each refresh replaces the cookie and + * deletes the token it was sent with, so two tabs refreshing at once would send + * the same cookie and the second would be refused - signing that tab out. Behind + * the lock the second tab sends the cookie the first one just got, or, when the + * first tab's new Jwt has already arrived over the channel, does not refresh at all. */ export function silentRefresh(): Promise { if (!refreshInFlight) { - refreshInFlight = (async () => { + const tokenBefore = accessToken; + const refresh = async (): Promise => { + if (accessToken && accessToken !== tokenBefore) return accessToken; try { const res = await fetch(`${API_BASE}/accounts/login`, { method: "POST", @@ -58,7 +140,11 @@ export function silentRefresh(): Promise { } catch { return null; } - })(); + }; + refreshInFlight = (async () => + typeof navigator !== "undefined" && navigator.locks + ? navigator.locks.request("bitween-refresh", refresh) + : refresh())(); void refreshInFlight.finally(() => { refreshInFlight = null; }); @@ -78,24 +164,29 @@ async function toApiError(res: Response): Promise { // 404 → the message is a bare JSON string. if (typeof body === "string" && body) - return new ApiRequestError(res.status === 404 ? "NOT_FOUND" : "ERROR", body); + return new ApiRequestError(res.status === 404 ? "NOT_FOUND" : "ERROR", body, res.status); // Framework-level errors (415, unhandled 500, …) come as ASP.NET ProblemDetails: // { type, title, status, traceId }. Prefer its human `title`. if (body && typeof body === "object" && "title" in body && "status" in body) { const pd = body as { title?: string; status?: number }; - return new ApiRequestError(`HTTP_${pd.status ?? res.status}`, pd.title || `Request failed (${res.status}).`); + return new ApiRequestError(`HTTP_${pd.status ?? res.status}`, pd.title || `Request failed (${res.status}).`, res.status); } // 400 → ASP.NET SerializableError: { key: [msg, ...] } (key is the validation // code for SWValidationException, or the exception type name otherwise). + // Every message is kept: the first key's first message used to be all that reached the form, + // so a save with two problems showed one, and the second only after fixing the first. if (body && typeof body === "object") { - const [code, value] = Object.entries(body as Record)[0] ?? []; - const message = Array.isArray(value) ? String(value[0]) : String(value ?? ""); - if (code) return new ApiRequestError(code, message || "Request failed."); + const errors: Record = {}; + for (const [key, value] of Object.entries(body as Record)) + errors[key] = (Array.isArray(value) ? value : [value]).filter((v) => v != null && v !== "").map(String); + const [code] = Object.keys(errors); + const messages = [...new Set(Object.values(errors).flat())]; + if (code) return new ApiRequestError(code, messages.join(" ") || "Request failed.", res.status, errors); } - return new ApiRequestError("ERROR", `Request failed (${res.status}).`); + return new ApiRequestError("ERROR", `Request failed (${res.status}).`, res.status); } export interface RequestOptions { @@ -103,7 +194,9 @@ export interface RequestOptions { body?: unknown; /** Internal: prevents the 401 → refresh → retry loop from recursing. */ _retried?: boolean; - /** Lets a caller cancel the request — only `logout` needs this, and says why. */ + /** A file sent as the body itself rather than as JSON: an adapter package. */ + file?: Blob; + /** Lets a caller cancel the request - only `logout` needs this, and says why. */ signal?: AbortSignal; } @@ -119,7 +212,7 @@ export async function request(path: string, opts: RequestOptions = {}): Promi const text = await res.text(); if (!text) return undefined as T; // Some endpoints return a bare string as text/plain (e.g. /partners/generatekey), - // which isn't valid JSON — only parse when the response actually is JSON. + // which isn't valid JSON - only parse when the response actually is JSON. const isJson = res.headers.get("content-type")?.includes("application/json") ?? false; return (isJson ? JSON.parse(text) : text) as T; } @@ -139,18 +232,19 @@ async function send(path: string, opts: RequestOptions): Promise { const token = getToken(); const method = opts.method ?? "GET"; // Backend command handlers (POST) bind a JSON body, so they always need - // `Content-Type: application/json` — even a body-less command like logout. + // `Content-Type: application/json` - even a body-less command like logout. // Without it the framework rejects the call with 415 before the handler runs; // an empty `{}` satisfies it. - const sendJson = method === "POST"; + const sendJson = method === "POST" && !opts.file; const res = await fetch(`${API_BASE}${path}`, { method, credentials: "include", headers: { ...(sendJson ? { "Content-Type": "application/json" } : {}), + ...(opts.file ? { "Content-Type": opts.file.type || "application/zip" } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), }, - body: sendJson ? JSON.stringify(opts.body ?? {}) : undefined, + body: opts.file ?? (sendJson ? JSON.stringify(opts.body ?? {}) : undefined), ...(opts.signal ? { signal: opts.signal } : {}), }); @@ -173,14 +267,19 @@ export const post = (path: string, body?: unknown): Promise => request(path, { method: "POST", body }); /** - * For a secondary read that only enriches a page — the "used by" counts, which come from another + * For a secondary read that only enriches a page - the "used by" counts, which come from another * area's list. Those are permission-guarded in their own right, so a role that can see this page * but not that area would otherwise take the whole page down with it. The enrichment is worth * losing; the page isn't. Only refusals are swallowed, so a real outage still surfaces. */ -export async function getEnrichment(path: string, fallback: T): Promise { +export function getEnrichment(path: string, fallback: T): Promise { + return enrichment(get(path), fallback); +} + +/** `getEnrichment` for a read that is more than one GET - another area's whole list, say. */ +export async function enrichment(read: Promise, fallback: T): Promise { try { - return await get(path); + return await read; } catch (e) { // A refusal for *this* read only. UNAUTHENTICATED deliberately isn't swallowed: that one means // the session itself is gone, and the app needs to hear about it. diff --git a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts index 05333eb1..7d8d01f4 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts @@ -26,7 +26,7 @@ interface RawRetryPolicyRow { id: number; name: string; groupCount: number; - /** Counted by the backend — see RetryPolicyRow.UsedByCount. */ + /** Counted by the backend - see RetryPolicyRow.UsedByCount. */ usedByCount: number; } interface RawRetryPolicy { @@ -282,7 +282,7 @@ export const retryPolicyMethods = { }, ): Promise { // Update replaces the whole policy, so every field it accepts has to be sent back. Omitting - // the alert cleared it on the server on every save — the settings were still on screen, and + // the alert cleared it on the server on every save - the settings were still on screen, and // gone from the database. await post(`/retrypolicies/${id}`, { name: changes.name, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts index 30887a81..dfc84f6b 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts @@ -1,5 +1,5 @@ /** - * Builds a Searchy query string — the `filter=Field:Rule:Value` + `page`/`size` + * Builds a Searchy query string - the `filter=Field:Rule:Value` + `page`/`size` * convention every list endpoint in this API already speaks (see `buildExchangeQuery` * for the hand-written original this generalizes). * @@ -15,7 +15,7 @@ * `URLSearchParams.toString()` form-encodes a space as "+", but the backend parses this query * string with `Uri.UnescapeDataString` (SW.PrimitiveTypes' QueryStringParser), which decodes * %XX escapes and leaves "+" as a literal plus. So every multi-word term silently matched - * nothing: searching "Order intake" looked for "Order+intake". Only `filter=` is affected — + * nothing: searching "Order intake" looked for "Order+intake". Only `filter=` is affected - * endpoints whose parameters are model-bound go through ASP.NET's own parser, which reads "+" * as a space correctly. * @@ -26,9 +26,9 @@ export const searchyQueryString = (params: URLSearchParams): string => params.toString().replace(/\+/g, "%20"); export function buildListQuery(opts: { - /** [Field, Rule, Value] triples — Rule 1 = EqualsTo, 4 = Contains. Skipped when Value is "". */ + /** [Field, Rule, Value] triples - Rule 1 = EqualsTo, 4 = Contains. Skipped when Value is "". */ filters?: [string, number, string | number][]; - /** [Field, Order] — Order 1 = ascending, 2 = descending. */ + /** [Field, Order] - Order 1 = ascending, 2 = descending. */ sort?: [string, number]; offset: number; limit: number; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/serviceAccounts.ts b/SW.Bitween.Web/ClientApp/src/api/http/serviceAccounts.ts new file mode 100644 index 00000000..ea126b8e --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/serviceAccounts.ts @@ -0,0 +1,32 @@ +import type { IssuedServiceToken, ServiceAccount } from "../types"; +import { get, post, request } from "./request"; + +export const serviceAccountMethods = { + async listServiceAccounts(): Promise { + return (await get("/serviceaccounts")) ?? []; + }, + + async createServiceAccount(input: { name: string; roleIds: number[] }): Promise { + return await post("/serviceaccounts", input); + }, + + async updateServiceAccount(id: number, input: { name: string; roleIds: number[] }): Promise { + await post(`/serviceaccounts/${id}`, input); + }, + + async setServiceAccountDisabled(id: number, disabled: boolean): Promise { + await post(`/serviceaccounts/${id}/setDisabled`, { disabled }); + }, + + async deleteServiceAccount(id: number): Promise { + await request(`/serviceaccounts/${id}`, { method: "DELETE" }); + }, + + async issueServiceToken(id: number, input: { name: string; expiresInDays: number }): Promise { + return await post(`/serviceaccounts/${id}/issueToken`, input); + }, + + async revokeServiceToken(id: number, tokenId: number): Promise { + await post(`/serviceaccounts/${id}/revokeToken`, { tokenId }); + }, +}; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/session.ts b/SW.Bitween.Web/ClientApp/src/api/http/session.ts index ea9378a1..596a0bec 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/session.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/session.ts @@ -1,8 +1,8 @@ import { ApiRequestError, type Session, type User } from "../types"; import { getAppConfig } from "./appConfig"; -import { clearToken, get, getToken, post, request, setToken } from "./request"; +import { clearToken, get, getToken, mayHaveSession, post, request, setToken, silentRefresh } from "./request"; -/** GET /accounts/profile — camelCase ProfileModel. */ +/** GET /accounts/profile - camelCase ProfileModel. */ interface Profile { id: number; email: string; @@ -48,13 +48,13 @@ const loadSession = async (): Promise => buildSession(await get { - // No stored Jwt → anonymous; don't probe the backend (an expired token still - // gets refreshed via cookie inside request() on its 401). The token is only - // cleared on logout or an unrecoverable 401, so returning users keep it. - if (!getToken()) return null; + // The Jwt lives in memory, so a reload or a new tab starts without one. When this + // browser held a session, the refresh cookie gets a new one; when it never did, + // don't probe the backend - that would spend the sign-in rate limit on page loads. + if (!getToken()) { + if (!mayHaveSession()) return null; + if (!(await silentRefresh())) return null; + } try { return await loadSession(); } catch (e) { @@ -132,10 +135,10 @@ export const sessionMethods = { async logout(): Promise { // Cleared first and unconditionally. This used to run in a `finally`, which - // deleted the Jwt and then let the error through — the worst pairing, because + // deleted the Jwt and then let the error through - the worst pairing, because // the caller aborted before it could end the session and the app carried on - // rendering as if signed in. Removing the key here is also what wakes the - // other tabs (see the `storage` listener in SessionContext). + // rendering as if signed in. Clearing it here is also what tells the other + // tabs (see `onSignedOutElsewhere` in SessionContext). clearToken(); const controller = new AbortController(); logoutInFlight = controller; @@ -144,8 +147,8 @@ export const sessionMethods = { } catch { // Swallowed on purpose: signing out must not depend on the server answering, // and a sign-in aborting this is a normal outcome rather than a fault. Only - // the server can invalidate the refresh cookie — it is HttpOnly, so JS cannot - // touch it — so if this never lands the cookie outlives the sign-out. This + // the server can invalidate the refresh cookie - it is HttpOnly, so JS cannot + // touch it - so if this never lands the cookie outlives the sign-out. This // browser has no Jwt to pair with it, and the next sign-in replaces it. } finally { if (logoutInFlight === controller) logoutInFlight = null; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/settings.ts b/SW.Bitween.Web/ClientApp/src/api/http/settings.ts index 44c2e3b0..38a674a2 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/settings.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/settings.ts @@ -1,18 +1,28 @@ import type { ApiClient } from "../client"; -import type { AggregationRetentionCheck, RetentionProposal, RetentionStatus, Schedule, SettingRow } from "../types"; +import type { AggregationRetentionCheck, ClusterNodes, InstanceAbout, RetentionProposal, RetentionStatus, Schedule, SettingRow } from "../types"; import { get, post, request } from "./request"; import { toRawSchedules } from "./subscriptionBody"; export const settingsMethods = { /** * The backend owns the catalog (label, section, kind, default, secret), so rows arrive - * ready to render — including which section each belongs to and, for secrets, only + * ready to render - including which section each belongs to and, for secrets, only * whether a value is set. */ listSettings(): Promise { return get("/settings"); }, + /** The nodes, as their heartbeats describe them. */ + getClusterNodes(): Promise { + return get("/cluster/nodes"); + }, + + /** This Bitween and how it is set up: version, node, health, runtimes, effective limits. */ + getAbout(): Promise { + return get("/settings/about"); + }, + getRetention(refresh = false): Promise { return get(`/retention${refresh ? "?refresh=true" : ""}`); }, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts index 436fef9f..a961c6f7 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts @@ -6,7 +6,7 @@ import { toRawMatchExpression } from "./matchExpression"; * the gateway endpoints can reach. * * It lives here rather than in `subscriptions.ts` because `gateways.ts` needs it too, - * and `subscriptions.ts` already imports `gateways.ts` — putting it there would make + * and `subscriptions.ts` already imports `gateways.ts` - putting it there would make * that cycle mutual. */ diff --git a/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts index a9aab2bf..3653367c 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts @@ -2,6 +2,7 @@ import type { ApiClient } from "../client"; import { ApiRequestError, type AggregationTarget, + type ExpectTraffic, type SourcePaths, type Subscription, type SubscriptionDetail, @@ -36,7 +37,7 @@ import { type RawSchedule, } from "./subscriptionBody"; -// ——— backend shapes (camelCase over the wire) ——— +// --- backend shapes (camelCase over the wire) --- interface SearchyResponse { result: T[]; totalCount: number; @@ -97,6 +98,14 @@ interface RawSubscription { receiveOn: string | null; aggregateOn: string | null; pausedOn: string | null; + pausedAutomatically?: boolean; + autoPauseAfterFailures?: number | null; + expectTraffic?: ExpectTraffic | null; + quietSince?: string | null; + receiverVersion?: string | null; + validatorVersion?: string | null; + mapperVersion?: string | null; + handlerVersion?: string | null; isRunning: boolean | null; consecutiveFailures: number; lastException: string | null; @@ -129,7 +138,7 @@ const toSubscriptionType = (t: number | string): SubscriptionType => // Empty-valued properties are dropped: an adapter property with no value means // "not set", and keeping it would make a freshly-cleared field compare unequal to -// stored data that never had the key — leaving the Save bar up after an undo. +// stored data that never had the key - leaving the Save bar up after an undo. const toRecord = (kvs: RawKeyAndValue[] | null): Record => Object.fromEntries((kvs ?? []).filter((kv) => kv.value !== "").map((kv) => [kv.key, kv.value])); @@ -143,8 +152,8 @@ const toSchedules = (raw: RawSchedule[] | null): Schedule[] => })); /** - * When the schedule fires next. The two scheduled types keep it in different columns — - * `ReceiveOn` for Receiving, `AggregateOn` for Aggregation — and only ever one of them is + * When the schedule fires next. The two scheduled types keep it in different columns - + * `ReceiveOn` for Receiving, `AggregateOn` for Aggregation - and only ever one of them is * set, so taking whichever exists lets every screen read one field. */ const nextRunOf = (raw: RawSubscription): string | null => raw.receiveOn ?? raw.aggregateOn ?? null; @@ -158,8 +167,18 @@ function toSubscription(raw: RawSubscription, idOverride?: number): Subscription partnerId: raw.partnerId, enabled: !raw.inactive, pausedOn: raw.pausedOn ?? null, + pausedAutomatically: raw.pausedAutomatically ?? false, + receiverVersion: raw.receiverVersion ?? null, + validatorVersion: raw.validatorVersion ?? null, + mapperVersion: raw.mapperVersion ?? null, + handlerVersion: raw.handlerVersion ?? null, + autoPauseAfterFailures: raw.autoPauseAfterFailures ?? null, + expectTraffic: raw.expectTraffic ?? null, + quietSince: raw.quietSince ?? null, workGroupId: raw.workGroupId ?? null, + categoryId: raw.categoryId ?? null, retryPolicyId: raw.retryPolicyId ?? null, + customRetryGroups: customRetryGroups(raw.customRetryPolicy), receiverId: raw.receiverId ?? null, receiverProperties: toRecord(raw.receiverProperties), validatorId: raw.validatorId ?? null, @@ -202,6 +221,7 @@ type UpdatableFields = Partial< | "name" | "enabled" | "workGroupId" + | "categoryId" | "retryPolicyId" | "receiverId" | "receiverProperties" @@ -218,6 +238,12 @@ type UpdatableFields = Partial< | "responseMessageTypeName" | "runOnBadResponses" | "aggregationTarget" + | "autoPauseAfterFailures" + | "expectTraffic" + | "receiverVersion" + | "validatorVersion" + | "mapperVersion" + | "handlerVersion" > > & { /** Create this response subscription in the same save and hand the response to it. */ @@ -227,27 +253,39 @@ type UpdatableFields = Partial< /** * POST /subscriptions/{id} replaces the whole record, and Update.cs's model * (SubscriptionUpdate) carries several fields this UI never shows (categoryId, - * temporary, …) — omitting them would silently reset them to their type + * temporary, …) - omitting them would silently reset them to their type * default. So every write reads the current full record first * and splices `changes` on top of it, mirroring writePartner()'s pattern. * * Secret adapter property values arrive masked as the literal string * "__private__" (Get.cs's PrivateSentinel); passed straight through unedited, - * Update.cs's own merge logic restores the real stored value — this file + * Update.cs's own merge logic restores the real stored value - this file * never needs to know about the sentinel itself. */ +/** How many rule groups an inline retry policy has; null when there is none. */ +function customRetryGroups(policy: unknown): number | null { + if (!policy || typeof policy !== "object") return null; + const groups = (policy as { groups?: unknown[]; Groups?: unknown[] }).groups ?? (policy as { Groups?: unknown[] }).Groups; + return Array.isArray(groups) ? groups.length : 0; +} + async function applyChanges(id: number, current: RawSubscription, changes: UpdatableFields): Promise { await post(`/subscriptions/${id}`, { name: changes.name ?? current.name, documentId: current.documentId, partnerId: current.partnerId, aggregationForId: current.aggregationForId, - categoryId: current.categoryId, + categoryId: changes.categoryId !== undefined ? changes.categoryId : current.categoryId, inactive: changes.enabled !== undefined ? !changes.enabled : current.inactive, workGroupId: changes.workGroupId !== undefined ? changes.workGroupId : current.workGroupId, - // This UI only ever assigns a named policy, never an inline one. retryPolicyId: changes.retryPolicyId !== undefined ? changes.retryPolicyId : current.retryPolicyId, - customRetryPolicy: null, + // This UI assigns named policies only. A policy written on the subscription itself, through + // the API, is kept unless a different named one is chosen: sending null for it, as every + // save used to, erased it on a rename. + customRetryPolicy: + changes.retryPolicyId !== undefined && changes.retryPolicyId !== current.retryPolicyId + ? null + : (current.customRetryPolicy ?? null), receiverId: changes.receiverId !== undefined ? changes.receiverId : current.receiverId, receiverProperties: toKvArray(changes.receiverProperties ?? toRecord(current.receiverProperties)), validatorId: changes.validatorId !== undefined ? changes.validatorId : current.validatorId, @@ -266,6 +304,18 @@ async function applyChanges(id: number, current: RawSubscription, changes: Updat responseMessageTypeName: changes.responseMessageTypeName !== undefined ? changes.responseMessageTypeName : current.responseMessageTypeName, runOnBadResponses: changes.runOnBadResponses ?? current.runOnBadResponses ?? false, + // Pins carried over like the rest: left out, the update would unpin every adapter. + receiverVersion: changes.receiverVersion !== undefined ? changes.receiverVersion : (current.receiverVersion ?? null), + validatorVersion: changes.validatorVersion !== undefined ? changes.validatorVersion : (current.validatorVersion ?? null), + mapperVersion: changes.mapperVersion !== undefined ? changes.mapperVersion : (current.mapperVersion ?? null), + handlerVersion: changes.handlerVersion !== undefined ? changes.handlerVersion : (current.handlerVersion ?? null), + // Carried over like the rest: left out, the update would switch auto-pause off. + autoPauseAfterFailures: + changes.autoPauseAfterFailures !== undefined + ? changes.autoPauseAfterFailures + : (current.autoPauseAfterFailures ?? null), + // The same: left out, the update would drop the expectation. quietSince is the server's alone. + expectTraffic: changes.expectTraffic !== undefined ? changes.expectTraffic : (current.expectTraffic ?? null), newResponseSubscription: newResponseSubscriptionBody(changes.newResponseSubscription), temporary: current.temporary, aggregationTarget: @@ -324,8 +374,19 @@ export const subscriptionMethods = { partnerIds: raw.partnerId !== null ? [raw.partnerId] : [], informationTypeId: raw.documentId, workGroupId: raw.workGroupId ?? null, + categoryId: raw.categoryId ?? null, retryPolicyId: raw.retryPolicyId ?? null, handlerId: raw.handlerId ?? null, + adapterUses: ( + [ + ["receiver", raw.receiverId, raw.receiverVersion], + ["validator", raw.validatorId, raw.validatorVersion], + ["mapper", raw.mapperId, raw.mapperVersion], + ["handler", raw.handlerId, raw.handlerVersion], + ] as const + ) + .filter(([, adapterId]) => !!adapterId) + .map(([kind, adapterId, version]) => ({ kind, adapterId: adapterId!, version: version ?? null })), responseMessageTypeName: raw.responseMessageTypeName ?? null, responseSubscriptionId: raw.responseSubscriptionId ?? null, // No backend endpoint indexes reference tokens, but the search rows carry @@ -357,6 +418,7 @@ export const subscriptionMethods = { type: SubscriptionType | null; informationTypeId?: number | null; partnerId?: number | null; + categoryId?: number | null; inactive?: boolean | null; offset: number; limit: number; @@ -367,6 +429,7 @@ export const subscriptionMethods = { ["Type", SEARCHY_RULE.equalsTo, query.type ?? ""], ["DocumentId", SEARCHY_RULE.equalsTo, query.informationTypeId ?? ""], ["PartnerId", SEARCHY_RULE.equalsTo, query.partnerId ?? ""], + ["CategoryId", SEARCHY_RULE.equalsTo, query.categoryId ?? ""], ["Inactive", SEARCHY_RULE.equalsTo, query.inactive == null ? "" : String(query.inactive)], ], sort: ["Name", SEARCHY_SORT.asc], @@ -431,11 +494,11 @@ export const subscriptionMethods = { type: SubscriptionType; name: string; /** - * Ignored for Aggregation — the backend constructor forces the built-in + * Ignored for Aggregation - the backend constructor forces the built-in * "Aggregation Document" whatever the caller sends, so there is nothing to pick. */ informationTypeId: number; - /** Required by the types that carry their own partner — Internal, ApiCall and Aggregation. */ + /** Required by the types that carry their own partner - Internal, ApiCall and Aggregation. */ partnerId?: number | null; /** Aggregation only: whose exchanges get rolled up. Required, and fixed once created. */ aggregationForId?: number | null; @@ -461,7 +524,7 @@ export const subscriptionMethods = { enabled?: boolean; }): Promise { // One call, one transaction. This used to be a POST followed by a PATCH, - // because create accepted only the name/type/document — and since the POST + // because create accepted only the name/type/document - and since the POST // committed on its own, a rejected PATCH left an empty subscription behind. const id = await post("/subscriptions", { name: input.name, @@ -510,6 +573,11 @@ export const subscriptionMethods = { return toSubscription(await fetchRaw(id), id); }, + /** Clears a running flag a dead run left behind, so the next scheduled run starts. */ + async clearRunning(id: number): Promise<{ cleared: boolean }> { + return post<{ cleared: boolean }>(`/subscriptions/${id}/clearrunning`, {}); + }, + async receiveNow(id: number): Promise { await post(`/subscriptions/${id}/receivenow`, {}); return toSubscription(await fetchRaw(id), id); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/team.ts b/SW.Bitween.Web/ClientApp/src/api/http/team.ts index c00f919b..e978ab6a 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/team.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/team.ts @@ -27,6 +27,8 @@ interface RawAccount { /** Set by the backend's failed-sign-in lockout; null or in the past means not locked. */ lockoutEnd: string | null; createdOn: string; + lastSignInOn?: string | null; + microsoftLinked?: boolean; roles: RawRoleSummary[] | null; } @@ -48,9 +50,10 @@ const toUser = (r: RawAccount): User => ({ status: r.disabled ? "disabled" : "active", // Past lockouts are not state anyone can act on, so they read as no lockout at all. lockedUntil: r.lockoutEnd && new Date(r.lockoutEnd) > new Date() ? r.lockoutEnd : null, - // Not tracked by the backend: no login-method projection, no last-seen column. - microsoftLinked: false, + microsoftLinked: r.microsoftLinked ?? false, createdOn: r.createdOn, + // The last sign-in, not the last request: a session renewing itself doesn't count. + lastActiveOn: r.lastSignInOn ?? undefined, }); const toRole = (r: RawRole): Role => ({ @@ -63,7 +66,7 @@ const toRole = (r: RawRole): Role => ({ memberCount: r.memberCount, }); -/** One page big enough for any realistic team — the backend defaults to 20. */ +/** One page big enough for any realistic team - the backend defaults to 20. */ const ALL_MEMBERS = 500; async function fetchUsers(): Promise { @@ -79,12 +82,12 @@ async function fetchUser(id: string): Promise { } export const teamMethods = { - // — permission catalog — + // - permission catalog - async getPermissionCatalog(): Promise { return await get("/permissions"); }, - // — members — + // - members - listUsers: fetchUsers, getUser: fetchUser, @@ -132,7 +135,7 @@ export const teamMethods = { await post(`/accounts/${id}/remove`, {}); }, - // — roles — + // - roles - async listRoles(): Promise { const res = await get>("/roles?pageSize=200"); return (res.result ?? []).map(toRole); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts index d283596c..218ccb1d 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts @@ -7,7 +7,7 @@ import { type WorkGroupRow, type Paged, } from "../types"; -import { get, post } from "./request"; +import { enrichment, get, post } from "./request"; interface SearchyResponse { result: T[]; @@ -19,7 +19,7 @@ interface RawWorkGroup { busMessageName: string; options: { rabbitMqOptions: { prefetch: number | null; priority: number | null } | null } | null; processorNodeCount: number | null; - /** Counted by the backend — see WorkGroupModel.UsedByCount. */ + /** Counted by the backend - see WorkGroupModel.UsedByCount. */ usedByCount: number; } interface RawSubscriptionRef { @@ -77,7 +77,7 @@ const toWorkGroup = (w: RawWorkGroup): WorkGroup => ({ // The backend's own default kicks in only when the caller omits the param // entirely (`request.Limit ??= 20`), so passing an explicit, generously large -// one is how "everything" is asked for — there is no separate "unbounded" +// one is how "everything" is asked for - there is no separate "unbounded" // value the way the shared Searchy endpoints have with size=0. const EVERYTHING = 1_000_000; @@ -124,7 +124,9 @@ export const workGroupMethods = { }, async getWorkGroup(id: number): Promise { - const [rows, subs] = await Promise.all([fetchRows(), fetchSubscriptionsByWorkGroup(id)]); + // The subscriptions using it are context, from an area a role that can see work groups may not + // be allowed into; without them the page still has everything it edits. + const [rows, subs] = await Promise.all([fetchRows(), enrichment(fetchSubscriptionsByWorkGroup(id), [])]); const w = rows.find((x) => x.id === id); if (!w) throw new ApiRequestError("NOT_FOUND", "This work group no longer exists."); return { diff --git a/SW.Bitween.Web/ClientApp/src/api/index.ts b/SW.Bitween.Web/ClientApp/src/api/index.ts index 5e8fa302..6ca8da09 100644 --- a/SW.Bitween.Web/ClientApp/src/api/index.ts +++ b/SW.Bitween.Web/ClientApp/src/api/index.ts @@ -3,14 +3,13 @@ import { httpClient } from "./http/httpClient"; /** * The swap point. Everything in the UI imports `api` from here. - * This is the real HTTP client — no mock, no toggle. Domains not yet wired - * reject with NotWiredError so their screens read as honestly-not-connected - * (see BACKEND_WIRING_PLAN §2). + * This is the real HTTP client - no mock, no toggle. */ export const api: ApiClient = httpClient; export { getAppConfig, resetAppConfig } from "./http/appConfig"; +export { grantCliSignIn } from "./http/cliSignIn"; export type { AppConfig } from "./http/appConfig"; export { referencesGlobal, referencesPartnerProp } from "./http/references"; -export { TOKEN_KEY, onSessionEnded } from "./http/request"; +export { onSessionEnded, onSignedOutElsewhere } from "./http/request"; export * from "./types"; diff --git a/SW.Bitween.Web/ClientApp/src/api/metricsTypes.ts b/SW.Bitween.Web/ClientApp/src/api/metricsTypes.ts new file mode 100644 index 00000000..e5fa9016 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/metricsTypes.ts @@ -0,0 +1,212 @@ +/* + * The metrics endpoints' answers (GET /api/metrics/...). Every number is read from hourly rows + * the server keeps; "since" is when they begin, so a page can say so rather than show an empty + * past as no traffic. + */ + +export interface ExchangeSummary { + total: number; + succeeded: number; + badResponse: number; + failed: number; + retries: number; + manualRetries: number; + /** Share of finished exchanges that succeeded; null without any. */ + successRate: number | null; + p50Ms: number | null; + p95Ms: number | null; + bytes: number; +} + +export type HealthStatus = "healthy" | "degraded" | "failing" | "silent" | "idle" | "paused" | "inactive"; + +export interface SilentSubscription { + subscriptionId: number; + name: string; + type: string; + lastExchangeHour: string | null; + quietHours: number | null; + usualGapHours: number | null; +} + +export interface MetricsOverview { + since: string | null; + hourly: { hour: string; total: number; succeeded: number; badResponse: number; failed: number }[]; + weekAgo: { hour: string; total: number }[]; + last24h: ExchangeSummary; + previous24h: ExchangeSummary; + last7d: ExchangeSummary; + daily: { day: string; total: number; succeeded: number; badResponse: number; failed: number; bytes: number }[]; + topFailing: { subscriptionId: number; name: string | null; failed: number; total: number }[]; + busiestSubscriptions: { + subscriptionId: number; + name: string | null; + total: number; + failed: number; + badResponse: number; + }[]; + busiestPartners: { partnerId: number; name: string | null; total: number; failed: number }[]; + slowestAdapters: { adapterId: string; calls: number; p95Ms: number | null; failureRate: number | null }[]; + silent: SilentSubscription[]; + failing: number; + degraded: number; + backlog: { at: string; queueDepth: number; retryBacklog: number; deadLetterBacklog: number }[]; +} + +export interface AdapterUsageTotals { + calls: number; + failures: number; + failureRate: number | null; + timeouts: number; + limitStops: number; + crashes: number; + p50Ms: number | null; + p95Ms: number | null; + maxMs: number; +} + +export interface AdapterUsage extends AdapterUsageTotals { + id: string; + lastCalledOn: string | null; + daily: { day: string; calls: number; failures: number }[]; +} + +export interface AdapterUsageList { + since: string | null; + days: number; + adapters: AdapterUsage[]; + /** Called in the last 30 days but not in the window. */ + quiet: { id: string; lastCalledOn: string }[]; +} + +export interface ErrorGroup { + fingerprint: string; + count: number; + lastSeenOn: string; + sample: string | null; +} + +export interface AdapterUsageDetail { + id: string; + days: number; + since: string | null; + total: AdapterUsageTotals; + versions: { version: string; totals: AdapterUsageTotals }[]; + nodes: { node: string; totals: AdapterUsageTotals }[]; + roles: { role: string; runtime: string; totals: AdapterUsageTotals }[]; + daily: { day: string; calls: number; failures: number; p95Ms: number | null }[]; + errors: ErrorGroup[]; + subscriptions: { + subscriptionId: number; + name: string; + type: string; + exchanges: number; + failed: number; + bytes: number; + }[]; +} + +export interface SubscriptionHealthRow { + subscriptionId: number; + name: string; + type: string; + status: HealthStatus; + successRate: number | null; + p95Ms: number | null; + lastExchangeHour: string | null; + quietHours: number | null; + usualGapHours: number | null; + daily: number[]; + total: number; + failed: number; + badResponse: number; + retries: number; + manualRetries: number; + bytes: number; +} + +export interface SubscriptionHealthList { + since: string | null; + days: number; + subscriptions: SubscriptionHealthRow[]; +} + +export interface SubscriptionMetrics { + id: number; + days: number; + since: string | null; + status: HealthStatus | null; + lastExchangeHour: string | null; + quietHours: number | null; + usualGapHours: number | null; + total: ExchangeSummary & { failedAtMapper: number; failedAtHandler: number }; + hourly: { hour: string; total: number; failed: number; badResponse: number }[]; + daily: { day: string; total: number; failed: number; p95Ms: number | null }[]; + nodes: { node: string; total: number; failed: number }[]; + errors: ErrorGroup[]; +} + +export interface PartnerUsage { + partnerId: number; + name: string; + exchanges: number; + failed: number; + badResponse: number; + successRate: number | null; + bytes: number; + lastExchangeHour: string | null; + keyLastUsedOn: string | null; +} + +export interface PartnerUsageList { + since: string | null; + days: number; + partners: PartnerUsage[]; +} + +export interface ResidentInstance { + /** adapter id, state, pid, memory MB, CPU %, uptime s, restarts, quarantined, in flight, last error. */ + a: string; + s: string; + pid: number | null; + mb: number; + cpu: number; + up: number; + r: number; + q: boolean; + f: number; + e: string | null; +} + +export interface NodeMetrics { + nodes: { + id: string; + host: string; + alive: boolean; + lastSeenOn: string; + exchanges: number; + failed: number; + successRate: number | null; + p95Ms: number | null; + adapterCalls: number; + adapterFailures: number; + timeouts: number; + limitStops: number; + crashes: number; + resident: ResidentInstance[]; + }[]; +} + +export interface Insight { + kind: string; + severity: "info" | "warn"; + title: string; + detail: string; + count: number; + items: { label: string; href: string }[]; +} + +export interface Insights { + since: string | null; + insights: Insight[]; +} diff --git a/SW.Bitween.Web/ClientApp/src/api/permissions.ts b/SW.Bitween.Web/ClientApp/src/api/permissions.ts index cf09e206..a3c70fae 100644 --- a/SW.Bitween.Web/ClientApp/src/api/permissions.ts +++ b/SW.Bitween.Web/ClientApp/src/api/permissions.ts @@ -8,7 +8,7 @@ import { keys } from "./queryKeys"; * and served by GET /permissions. It deliberately does not live here as well: a second copy would * silently drift, and the role matrix would start offering grants the API ignores. * - * What stays here is presentation — how an action is worded and the order columns appear in. + * What stays here is presentation - how an action is worded and the order columns appear in. */ export const ACTION_LABELS: Record = { view: "View", diff --git a/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts b/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts index 2b1b8cf8..60946f48 100644 --- a/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts +++ b/SW.Bitween.Web/ClientApp/src/api/queryKeys.ts @@ -6,11 +6,11 @@ import type { QueryClient } from "@tanstack/react-query"; * Keys are hierarchical and grouped by *entity*, not by endpoint: a subscription's detail, its * paged rows and the whole-list cache all start with `["subscriptions"]`. React Query matches * invalidation by prefix, so `invalidateQueries({ queryKey: keys.subscriptions.all })` clears the - * lot — one line where the old hand-written string keys needed one line per variant, and where + * lot - one line where the old hand-written string keys needed one line per variant, and where * missing one was silent. * * It was not hypothetical: nothing in the app ever invalidated `partners-search`, - * `information-types-search`, `retry-policies-search` or `api-gateways-search` — the four paged + * `information-types-search`, `retry-policies-search` or `api-gateways-search` - the four paged * tables people actually look at. They only appeared correct because the global 10s staleTime * expired almost immediately and a remount refetched. Two more pairs (`information-types` / * `information-types-all`, `partners` / `partners-all`) were the same fetch cached twice under @@ -25,7 +25,7 @@ export const keys = { all: ["subscriptions"] as const, /** The whole list, held once and shared by every "who uses this?" panel. */ cache: ["subscriptions", "cache"] as const, - /** Live status for every subscription, keyed by id — shared by the gateway pages. */ + /** Live status for every subscription, keyed by id - shared by the gateway pages. */ rows: ["subscriptions", "rows"] as const, rowsSearch: (params: Record) => ["subscriptions", "rows", params] as const, detail: (id: number | string | null | undefined) => ["subscriptions", "detail", id] as const, @@ -59,6 +59,7 @@ export const keys = { list: ["api-gateways", "list"] as const, search: (params: Record) => ["api-gateways", "search", params] as const, detail: (id: number | string) => ["api-gateways", "detail", id] as const, + rejections: (id: number | string) => ["api-gateways", "rejections", id] as const, attachments: (id: number | string, params: Record) => ["api-gateways", "attachments", id, params] as const, }, @@ -85,11 +86,11 @@ export const keys = { list: ["data-sources", "list"] as const, search: (params: Record) => ["data-sources", "search", params] as const, detail: (id: number | string) => ["data-sources", "detail", id] as const, - /** Live heartbeat, polled — deliberately its own key so refreshing it never refetches the form. */ + /** Live heartbeat, polled - deliberately its own key so refreshing it never refetches the form. */ telemetry: (id: number | string) => ["data-sources", "telemetry", id] as const, /** What the engine says it can do. Fixed for the life of a connection, so cached hard. */ capabilities: (id: number | string) => ["data-sources", "capabilities", id] as const, - /** One page of the catalog. The filters are part of the key — each is a different question. */ + /** One page of the catalog. The filters are part of the key - each is a different question. */ schema: (id: number | string, params: Record) => ["data-sources", "schema", id, params] as const, /** One object's columns or parameters, fetched only when its row is opened. */ @@ -142,6 +143,10 @@ export const keys = { detail: (id: number | string | null | undefined) => ["roles", "detail", id] as const, }, + serviceAccounts: { + all: ["service-accounts"] as const, + }, + users: { all: ["users"] as const, list: ["users", "list"] as const, @@ -151,6 +156,8 @@ export const keys = { exchanges: { all: ["exchanges"] as const, search: (params: string) => ["exchanges", "search", params] as const, + breakdown: (params: string) => ["exchanges", "breakdown", params] as const, + newSince: (params: string) => ["exchanges", "newSince", params] as const, document: (key: string | null) => ["exchanges", "document", key] as const, /** * A chain never changes above the exchange asked about, and only grows below it, so this is @@ -174,11 +181,26 @@ export const keys = { }, queueHealth: ["queue-health"] as const, + categories: ["subscription-categories"] as const, + background: ["queue-health", "background"] as const, dashboard: ["dashboard"] as const, + metrics: { + all: ["metrics"] as const, + overview: ["metrics", "overview"] as const, + adapters: (days: number) => ["metrics", "adapters", days] as const, + adapter: (id: string, days: number) => ["metrics", "adapter", id, days] as const, + subscriptions: (days: number) => ["metrics", "subscriptions", days] as const, + subscription: (id: number, days: number) => ["metrics", "subscription", id, days] as const, + partners: (days: number) => ["metrics", "partners", days] as const, + nodes: ["metrics", "nodes"] as const, + insights: ["metrics", "insights"] as const, + }, settings: { all: ["settings"] as const, list: ["settings", "list"] as const, + about: ["settings", "about"] as const, + nodes: ["settings", "nodes"] as const, /** Under settings, so saving a setting refreshes what retention it implies. */ retention: ["settings", "retention"] as const, retentionPreview: (proposal: string) => ["settings", "retention", "preview", proposal] as const, @@ -191,6 +213,12 @@ export const keys = { /** Fixed for the lifetime of a deployment. */ permissionCatalog: ["permission-catalog"] as const, adapters: (kind: string) => ["adapters", kind] as const, + /** A published version's source never changes, so these are fixed too. */ + adapterSource: (adapterId: string, version: string) => ["adapter-source", adapterId, version] as const, + adapterSourceFile: (adapterId: string, version: string, path: string) => + ["adapter-source", adapterId, version, path] as const, + adapterDrafts: ["adapter-drafts"] as const, + adapterDraft: (id: number) => ["adapter-drafts", id] as const, } as const; const MINUTE = 60_000; @@ -200,17 +228,17 @@ const MINUTE = 60_000; * * Declared centrally rather than at 97 call sites, so the policy for an entity is one number in * one place. Mutations invalidate explicitly, so these windows only govern *background* - * refetching — an edit still shows up immediately. + * refetching - an edit still shows up immediately. * * Three tiers: - * - **Fixed per deployment** — never refetched. - * - **Reference/config data** — changes only when someone edits it here, and that path + * - **Fixed per deployment** - never refetched. + * - **Reference/config data** - changes only when someone edits it here, and that path * invalidates. Minutes are safe. - * - **Operational data** — moves on its own as messages flow. Always refetched on mount; most of + * - **Operational data** - moves on its own as messages flow. Always refetched on mount; most of * these screens also declare their own `refetchInterval`. */ export function applyQueryDefaults(queryClient: QueryClient): void { - const fixed = [keys.permissionCatalog, ["adapters"]]; + const fixed = [keys.permissionCatalog, ["adapters"], ["adapter-source"]]; const reference = [ keys.informationTypes.all, keys.partners.all, @@ -220,6 +248,7 @@ export function applyQueryDefaults(queryClient: QueryClient): void { keys.notifiers.all, keys.roles.all, keys.users.all, + keys.serviceAccounts.all, keys.apiGateways.all, keys.busGateways.all, keys.subscriptions.all, @@ -236,8 +265,8 @@ export function applyQueryDefaults(queryClient: QueryClient): void { keys.subscriptions.scheduleHealth, ["subscriptions", "receive-attempts"], ["subscriptions", "runs"], - // The row shape carries live state — is it running, how many consecutive failures, when it - // fires next — so the tables built on it have to refetch on mount like any other live view. + // The row shape carries live state - is it running, how many consecutive failures, when it + // fires next - so the tables built on it have to refetch on mount like any other live view. // (The whole-list `cache` below is a different query and stays held for the session.) keys.subscriptions.rows, // A history card sits on the same page as the form that writes to it, so it is never diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts index 40c66077..a6f76ddb 100644 --- a/SW.Bitween.Web/ClientApp/src/api/types.ts +++ b/SW.Bitween.Web/ClientApp/src/api/types.ts @@ -50,6 +50,39 @@ export interface User { lastActiveOn?: string; } +export type ServiceTokenStatus = "active" | "expired" | "revoked"; + +/** A service account's token as Bitween keeps it: never the value, only its first characters. */ +export interface ServiceToken { + id: number; + name: string; + /** "bwt_3fK9aB2x": enough to tell tokens apart. */ + shown: string; + createdOn: string; + createdBy: string | null; + expiresOn: string; + revokedOn: string | null; + lastUsedOn: string | null; + lastUsedFrom: string | null; + status: ServiceTokenStatus; +} + +/** The one answer that carries a token's value: the one that issued it. */ +export interface IssuedServiceToken extends ServiceToken { + token: string; +} + +/** An account for a program: signs in with tokens, never with a password or into the UI. */ +export interface ServiceAccount { + id: number; + name: string; + disabled: boolean; + createdOn: string; + lastSignInOn: string | null; + roles: { id: number; name: string }[]; + tokens: ServiceToken[]; +} + /** Just enough to name a role. Full definitions need the roles.view permission. */ export interface RoleSummary { id: string; @@ -59,11 +92,11 @@ export interface RoleSummary { export interface Session { user: User; roles: RoleSummary[]; - /** The union of every permission the user's roles grant — resolved by the backend. */ + /** The union of every permission the user's roles grant - resolved by the backend. */ permissions: PermissionKey[]; /** * The account still has the password it was created with. The backend grants such a token - * nothing, so `permissions` is empty and every screen would refuse — the app sends them to + * nothing, so `permissions` is empty and every screen would refuse - the app sends them to * change it instead of leaving them staring at an empty shell. */ mustChangePassword: boolean; @@ -76,25 +109,31 @@ export interface ApiError { export class ApiRequestError extends Error { code: string; - constructor(code: string, message: string) { + /** The HTTP status, when the error came from a response. */ + status?: number; + /** Every validation message by its code or field, when the server sent several. */ + errors?: Record; + constructor(code: string, message: string, status?: number, errors?: Record) { super(message); this.code = code; + this.status = status; + this.errors = errors; } } +/** A refusal that asking again won't change: not found, forbidden, invalid. Server and network failures may pass. */ +export const isPermanentError = (error: unknown): boolean => + error instanceof ApiRequestError && error.status !== undefined && error.status >= 400 && error.status < 500; + /** - * Thrown by any ApiClient method whose domain hasn't been wired to the real - * backend yet. Screens surface this as an honest "Not connected yet" state — - * never fake data. Batches remove these as they land. + * Whether a failed query is asked again. A refusal answers the same however often it is asked: + * retrying it only held "Loading…" on screen for the ~7s of backoff before the error showed. + * Server errors and dropped connections are retried, up to three times. */ -export class NotWiredError extends Error { - code = "NOT_WIRED"; - constructor(method: string) { - super(`"${method}" isn't connected to the backend yet.`); - } -} +export const shouldRetryQuery = (failureCount: number, error: unknown): boolean => + !isPermanentError(error) && failureCount < 3; -// ——— Configuration entities (sub-phase 2) ——— +// --- Configuration entities (sub-phase 2) --- /** Lightweight references for "used by" panels. */ export interface SubscriptionSetupRef { @@ -141,27 +180,34 @@ export interface SubscriptionInfo { type: SubscriptionType; /** * The subscription's OWN partner, which only the legacy types carry. Partners - * linked through a gateway attachment or a bus route are NOT here — the list + * linked through a gateway attachment or a bus route are NOT here - the list * endpoint doesn't know about them. Use `usePartnerSubscriptions()` when you * need the full picture. */ partnerIds: number[]; informationTypeId: number; workGroupId: number | null; + /** A label an operator files it under; null for none. */ + categoryId: number | null; retryPolicyId: number | null; /** - * Its delivery step. Null means nothing is delivered — and since a response is + * Its delivery step. Null means nothing is delivered - and since a response is * whatever the delivery hands back, null here means the two response fields below * can never be reached, however they are set. */ handlerId: string | null; + /** + * Every adapter it runs, one per configured slot, with the version it is pinned to (null + * follows the current one). What the Adapters page counts usage from. + */ + adapterUses: AdapterUse[]; /** The bus message its delivery response is published as, if any. */ responseMessageTypeName: string | null; /** The subscription its delivery response is handed straight to, if any. */ responseSubscriptionId: number | null; /** * Reference tokens found in its adapter properties. Both are matched - * case-insensitively, as the backend resolver does — compare them with + * case-insensitively, as the backend resolver does - compare them with * `referencesPartnerProp`/`referencesGlobal` rather than `===`. */ partnerPropKeys: string[]; @@ -189,8 +235,8 @@ export interface TrailEntry { entityName: string; entityKey: string; /** - * Groups everything one save changed. A single edit often touches several rows — a - * gateway and its routes — and this is what shows them as the one change they were. + * Groups everything one save changed. A single edit often touches several rows - a + * gateway and its routes - and this is what shows them as the one change they were. */ correlationId: string; changes: AuditChange[]; @@ -209,9 +255,11 @@ export interface AuditQuery { export interface ApiCredentialRef { name: string; - /** Only the first characters — full keys are shown once, at creation. */ + /** Only the first characters - full keys are shown once, at creation. */ keyPrefix: string; createdOn: string; + /** When a gateway last let a call through with it, to within a few minutes; null if none has been seen. */ + lastUsedOn?: string | null; } export interface Partner { @@ -236,7 +284,7 @@ export interface Partner { export interface PartnerRow extends Partner { credentialCount: number; usedByCount: number; - /** Property names only — the list endpoint never sends values, which may be secrets. */ + /** Property names only - the list endpoint never sends values, which may be secrets. */ propertyKeys: string[]; } export interface PartnerDetail extends Partner { @@ -261,6 +309,8 @@ export interface InformationType { disregardsUnfilteredMessages: boolean; /** Friendly name → JSONPath/XPath, matched by routes and filters. */ promotedProperties: { key: string; path: string }[]; + /** JSON Schema for a JSON type, XSD for an XML one; null checks nothing. */ + validationSchema: string | null; createdOn: string; /** * When this type was taken out of use, or null while it is still in use. A retired type keeps @@ -268,7 +318,7 @@ export interface InformationType { */ retiredOn: string | null; /** - * Seeded by the backend and referenced by machinery rather than by configuration — + * Seeded by the backend and referenced by machinery rather than by configuration - * the Aggregation Document. Can't be edited away, retired or deleted. */ isSystem: boolean; @@ -287,7 +337,7 @@ export interface GlobalValuesSet { id: string; name: string; values: Record; - /** Which value names are secrets — same sentinel round trip as Partner.secretProperties. */ + /** Which value names are secrets - same sentinel round trip as Partner.secretProperties. */ secretProperties: string[]; createdOn: string; } @@ -301,7 +351,7 @@ export interface GlobalValuesSetDetail extends GlobalValuesSet { usedBy: ValueSetUsage[]; } -// ——— Retry policies ——— +// --- Retry policies --- export type RetryResultType = "Error" | "BadResult"; @@ -346,7 +396,7 @@ export interface RetryGroup { /** OR logic; empty = any failure of the applicable kind. */ matchers: RetryMatcher[]; action: "Allow" | "Block"; - /** `maxAttemptsTotal: null` is no group-wide limit — only the per-message cap applies. */ + /** `maxAttemptsTotal: null` is no group-wide limit - only the per-message cap applies. */ budget?: { maxAttemptsPerError: number; maxAttemptsTotal: number | null; delay: RetryDelay }; notes?: string; /** Where this group's budget-exhausted alert goes, for every subscription using the policy. */ @@ -379,7 +429,7 @@ export interface RetryPolicyDetail extends RetryPolicy { /** * What became of a budget-exhausted alert. * - * `claimedOn` is when the alert was raised — all the counter itself records. Whether it then + * `claimedOn` is when the alert was raised - all the counter itself records. Whether it then * reached anyone is a separate fact that can fail, so the two are reported apart: a page showing * only the claim tells the reader someone was notified when nobody was. */ @@ -395,8 +445,8 @@ export interface RetryAlertOutcome { * The whole state of one subscription-and-group pair: how much of the group's budget that * subscription has spent, and where the pair's budget-exhausted alert would go. * - * Budgets are counted per pair — a shared policy gives every subscription its own separate total - * — so there is no such thing as "this policy's usage". Any single figure on a policy or a group + * Budgets are counted per pair - a shared policy gives every subscription its own separate total + * - so there is no such thing as "this policy's usage". Any single figure on a policy or a group * would be an aggregate matching nothing anyone can act on, which is why the pair is also what * resetting and overriding both address. */ @@ -409,20 +459,20 @@ export interface RetryUsageRow { total: number; /** Spent out: this subscription gets no further automatic retries from this group. */ exhausted: boolean; - /** Null when the pair has never failed — also how you know there is no counter to reset. */ + /** Null when the pair has never failed - also how you know there is no counter to reset. */ lastAttemptOn: string | null; /** Where the alert actually goes, or null when nothing sends for this pair. */ resolvedHandlerId: string | null; resolvedHandlerProperties: Record; resolvedFrom: RetryAlertLevel | null; - /** Which level deliberately switched the alert off — a decision, as against an oversight. */ + /** Which level deliberately switched the alert off - a decision, as against an oversight. */ silencedAt: RetryAlertLevel | null; /** This pair's own override; `Inherit` when it has none. */ override: RetryAlertConfig; alert: RetryAlertOutcome | null; } -/** One failure a group caught — what a usage row spent its budget on. */ +/** One failure a group caught - what a usage row spent its budget on. */ export interface RetryAttempt { exchangeId: string; /** How deep the retry chain was, 0 being the original delivery. Null for older failures. */ @@ -452,7 +502,7 @@ export interface RetryTestAttempt { reason: string; } -// ——— Notifiers ——— +// --- Notifiers --- export interface Notifier { id: number; @@ -482,10 +532,10 @@ export interface NotifierDetail extends Notifier { recentNotifications: NotificationEntry[]; } -// ——— Subscriptions (subscriptions) ——— +// --- Subscriptions (subscriptions) --- /** - * Backend Subscription.Type. Internal and ApiCall are legacy — shown and + * Backend Subscription.Type. Internal and ApiCall are legacy - shown and * editable, never created. */ @@ -496,7 +546,7 @@ export interface NotifierDetail extends Notifier { export type AggregationTarget = "Input" | "Output" | "Response"; /** * The editable fields of a subscription being defined inline, while whatever points - * at it is being made. Mirrors the studio's own draft — deliberately, so the canvas + * at it is being made. Mirrors the studio's own draft - deliberately, so the canvas * can hand its draft straight to the client. */ export interface InlineSubscriptionDraft { @@ -521,7 +571,7 @@ export interface InlineSubscriptionDraft { /** Response type only. */ runOnBadResponses?: boolean; /** - * A response subscription to create in the same save and hand the response to — instead + * A response subscription to create in the same save and hand the response to - instead * of `responseSubscriptionId`, never with it. */ newResponseSubscription?: NewResponseSubscriptionDraft | null; @@ -529,7 +579,7 @@ export interface InlineSubscriptionDraft { /** A response subscription being defined alongside the subscription that feeds it. */ export interface NewResponseSubscriptionDraft extends InlineSubscriptionDraft { - /** What the responses it runs on are — nothing imposes it, unlike a bus gateway's own. */ + /** What the responses it runs on are - nothing imposes it, unlike a bus gateway's own. */ informationTypeId: number; runOnBadResponses: boolean; } @@ -562,8 +612,110 @@ export interface AdapterInfo { label: string; /** Native adapters run in-process; others are deployed packages with versions. */ native: boolean; + /** Versions that can be pinned, oldest first. */ versions: string[]; props: AdapterProp[]; + /** From the adapter's manifest; empty for a package published before manifests. */ + summary?: string; + description?: string; + publisher?: string; + /** A data: URI. */ + icon?: string; + tags: string[]; + /** The version that runs when a subscription pins none; null when the catalog doesn't say. */ + currentVersion: string | null; + versionHistory: AdapterVersion[]; +} + +export interface AdapterUse { + kind: AdapterKind; + adapterId: string; + version: string | null; +} + +export interface AdapterVersion { + version: string; + publishedOn: string | null; + publishedBy: string | null; + releaseNotes: string | null; + withdrawn: boolean; + /** The package carries its source, which can be read and compared (adapter-source.view). */ + hasSource: boolean; + /** dotnet, python, node, exec; null when its manifest doesn't say. */ + runtime: string | null; +} + +/** The files a published version's source holds. Published versions never change, so neither does this. */ +export interface AdapterSourceListing { + adapterId: string; + version: string; + language: string | null; + runtime: string | null; + buildCommand: string | null; + lockfiles: string[]; + /** Relative to the source folder, sorted, each with its SHA-256 - enough to tell what two versions differ in. */ + files: { path: string; sha256: string }[]; +} + +export type DraftLanguage = "python" | "node" | "typescript"; + +export interface AdapterDraftSummary { + id: number; + adapterId: string; + language: DraftLanguage; + kind: string | null; + /** The version it started from or was last published as; null for an adapter not yet published. */ + baseVersion: string | null; + filesHash: string; + createdOn: string; + createdBy: string | null; + modifiedOn: string | null; + modifiedBy: string | null; +} + +export interface AdapterDraft extends AdapterDraftSummary { + files: Record; +} + +export interface DraftCheck { + name: string; + outcome: "Passed" | "Failed" | "Skipped"; + detail: string | null; +} + +/** What building a draft found: its problems, or what it is and how its contract checks went. */ +export interface DraftBuild { + succeeded: boolean; + conforms: boolean; + problems: string[]; + warnings: string[]; + checks: DraftCheck[]; + settings: { name: string; description: string | null; required: boolean; secret: boolean; default: string | null; type: string }[] | null; + commands: string[] | null; + lifecycle: string | null; + kinds: string[] | null; +} + +export interface DraftRun { + succeeded: boolean; + output: string | null; + error: string | null; + problems: string[]; +} + +export interface DraftPublishResult { + published: boolean; + version?: string; + build: DraftBuild; +} + +export interface AdapterSourceFile { + path: string; + sha256: string; + size: number; + /** Not text, or too large to show; `content` is then null. */ + binary: boolean; + content: string | null; } /** @@ -586,7 +738,7 @@ export type Recurrence = "Hourly" | "Daily" | "Weekly" | "Monthly"; export interface Schedule { recurrence: Recurrence; - /** Weekly: weekday 0–6 (Sun–Sat); Monthly: day of month 1–27; otherwise 0. */ + /** Weekly: weekday 0-6 (Sun-Sat); Monthly: day of month 1-27; otherwise 0. */ days: number; hours: number; minutes: number; @@ -594,19 +746,52 @@ export interface Schedule { backwards: boolean; } +/** The expectation that a subscription sees traffic regularly, checked by a server job. */ +export interface ExpectTraffic { + /** Quiet once no exchange arrives for this long inside the window; at least 5. */ + everyMinutes: number; + /** "Sun".."Sat"; null or empty is every day. */ + days: string[] | null; + /** "HH:mm" in `timeZone`, both or neither; no overnight windows, so `to` is later than `from`. */ + from: string | null; + to: string | null; + /** IANA id such as "Asia/Riyadh"; null is UTC. */ + timeZone: string | null; +} + export interface Subscription { id: number; name: string; type: SubscriptionType; informationTypeId: number; - /** Direct partner — legacy Internal/ApiCall (and Aggregation) only. */ + /** Direct partner - legacy Internal/ApiCall (and Aggregation) only. */ partnerId: number | null; /** Inverse of backend Inactive. Disabled = not scheduled, not matched. */ enabled: boolean; /** Paused still accepts work but holds it for later release. */ pausedOn: string | null; + /** Pinned adapter versions, one per slot; null follows the current version. */ + receiverVersion: string | null; + validatorVersion: string | null; + mapperVersion: string | null; + handlerVersion: string | null; + /** Paused by `autoPauseAfterFailures`, not by a person. */ + pausedAutomatically: boolean; + /** Pause after this many failed deliveries in a row; null never pauses. */ + autoPauseAfterFailures: number | null; + /** Traffic it should see regularly; null expects nothing. */ + expectTraffic: ExpectTraffic | null; + /** When the server found it quiet against `expectTraffic`; null while it is not. Read-only. */ + quietSince: string | null; workGroupId: number | null; + /** A label an operator files it under; null for none. */ + categoryId: number | null; retryPolicyId: number | null; + /** + * Rule groups of a retry policy written on the subscription itself, which only the API sets; + * null when it has none. The UI keeps it through a save unless a named policy is chosen. + */ + customRetryGroups?: number | null; receiverId: string | null; receiverProperties: Record; validatorId: string | null; @@ -630,16 +815,16 @@ export interface Subscription { /** Response type only: also run when the delivery that fed it came back bad. */ runOnBadResponses: boolean; /** - * Aggregation only: whose exchanges get rolled up. Fixed at creation — the backend + * Aggregation only: whose exchanges get rolled up. Fixed at creation - the backend * property has a private setter and the configuration applier deliberately skips it, * so no update can repoint a live roll-up. */ aggregationForId: number | null; /** Aggregation only. Meaningless for every other type, where it stays "Input". */ aggregationTarget: AggregationTarget; - // — health (read-only) — + // - health (read-only) - isRunning: boolean; - /** Receiving (and Aggregation) only — when the schedule will next fire, not when it last did. */ + /** Receiving (and Aggregation) only - when the schedule will next fire, not when it last did. */ nextReceiveOn: string | null; consecutiveFailures: number; lastException: string | null; @@ -660,7 +845,7 @@ export interface SubscriptionRow { lastException: string | null; scheduleSummary?: string; /** - * Receiving and Aggregation only — when the schedule will next fire, not when it last + * Receiving and Aggregation only - when the schedule will next fire, not when it last * did. The two types keep it in different columns on the backend (`ReceiveOn` vs * `AggregateOn`); this is whichever one applies. */ @@ -674,7 +859,7 @@ export interface SubscriptionRow { /** * One execution of a scheduled subscription, from the scheduler's own history. - * Kept for `RetentionDays` (~30) — older runs are purged, not archived here. + * Kept for `RetentionDays` (~30) - older runs are purged, not archived here. */ export interface SubscriptionRun { startedOn: string; @@ -695,7 +880,7 @@ export interface SubscriptionLastRun extends SubscriptionRun { recentSucceeded: number; } -/** One poll of a Receiving subscription's own receive step — independent of the scheduler's +/** One poll of a Receiving subscription's own receive step - independent of the scheduler's * run history, which only knows whether the method threw (it never does; failures here are * caught and reported this way instead). */ export type ReceiveOutcome = "Failed" | "NoNewData" | "Received"; @@ -741,7 +926,7 @@ export interface ScheduleHealth { state: "Normal" | "Paused" | "Blocked" | "Error" | "Complete" | "Missing"; /** The scheduler's own next fire time, computed independently of `nextReceiveOn`. */ nextFireOn: string | null; - /** Flagged as running with nothing executing — the concurrency guard is skipping every run. */ + /** Flagged as running with nothing executing - the concurrency guard is skipping every run. */ stuck: boolean; } @@ -781,14 +966,14 @@ export interface WorkGroupDetail extends WorkGroup { subscriptions: SubscriptionSetupRef[]; } -// ——— API gateways ——— +// --- API gateways --- export interface ApiGatewayAttachment { partnerId: number; partnerName: string; subscriptionId: number; subscriptionName: string; - /** null when the partner has none — it can't call a gateway that takes tokens. */ + /** null when the partner has none - it can't call a gateway that takes tokens. */ partnerLoginIdentity: string | null; } /** How a gateway's callers prove who they are: the gateway chooses, not the caller. */ @@ -821,9 +1006,12 @@ export interface ApiGatewayDetail extends ApiGateway { authentication: GatewayAuthentication; /** The system-wide key header (Settings → API behavior), used while `keyHeader` is empty. */ defaultKeyHeader: string; + /** What a sync call is told when its delivery fails. Legacy: 400 always. Accurate: 202, 422 or 502. */ + failureAnswer?: GatewayFailureAnswer; } +export type GatewayFailureAnswer = "Legacy" | "Accurate"; -// ——— Bus gateways ——— +// --- Bus gateways --- export interface BusGatewayRoute { id: number; @@ -843,7 +1031,7 @@ export interface BusGateway { createdOn: string; /** - * Where the messages come from. Null is Bitween's own internal bus — the only behaviour that + * Where the messages come from. Null is Bitween's own internal bus - the only behaviour that * existed before data sources, and still the default, so no gateway changes meaning. Set it and * this gateway is fed by a broker outside Bitween instead. */ @@ -855,10 +1043,10 @@ export interface BusGateway { endpoint: string | null; } -// ——— Data sources ——— +// --- Data sources --- /** - * A connection to something outside Bitween — today a broker. + * A connection to something outside Bitween - today a broker. * * It holds the connection and nothing else: what Bitween does with the messages belongs to the bus * gateway pointing at it, which is why one data source can feed many gateways the way one broker @@ -879,9 +1067,9 @@ export interface DataSource { */ deduplicationWindowDays: number; - /** Soft ceiling in MB — crossing it recycles the adapter between messages. 0 = host default. */ + /** Soft ceiling in MB - crossing it recycles the adapter between messages. 0 = host default. */ softMemoryLimitMb: number; - /** Hard ceiling in MB — becomes the adapter's GC heap hard limit. 0 = host default. */ + /** Hard ceiling in MB - becomes the adapter's GC heap hard limit. 0 = host default. */ hardMemoryLimitMb: number; /** @@ -917,13 +1105,13 @@ export interface DataSourceDetail extends DataSourceRow { /** * What the connection is doing right now, read from the adapter's heartbeat rather than from the - * data source row — which only carries what the last reconcile wrote back. + * data source row - which only carries what the last reconcile wrote back. */ /** * One named piece of SQL a data source may run. * * A record rather than a field on the data source, so that writing a query and changing the - * database credentials are different permissions — see the backend entity for why SQL cannot live + * database credentials are different permissions - see the backend entity for why SQL cannot live * on the subscription instead. */ export interface DataSourceStatement { @@ -959,7 +1147,7 @@ export interface DataSourceStatementUsage { export interface DataSourceStatementUsageEntry { subscriptionId: number; subscriptionName: string; - /** Handler, Mapper or Receiver — which adapter slot names it. */ + /** Handler, Mapper or Receiver - which adapter slot names it. */ role: string; /** query, execute or call. */ operation: string; @@ -977,7 +1165,7 @@ export interface DataSourceTelemetry { inFlight: number; lastError: string | null; - /** Whatever the adapter reports — per-queue depth, counters, prefetch. Untyped by design. */ + /** Whatever the adapter reports - per-queue depth, counters, prefetch. Untyped by design. */ details: Record; // Host-observed: these keep working when the adapter is wedged, which is when they matter. @@ -998,7 +1186,7 @@ export interface DataSourceTelemetry { export interface DataSourceInspectResult { ran: boolean; command: string | null; - /** The adapter's own JSON, as text — Bitween does not model any broker's topology. */ + /** The adapter's own JSON, as text - Bitween does not model any broker's topology. */ result: string | null; error: string | null; } @@ -1028,23 +1216,23 @@ export interface BusGatewayDetail extends BusGateway { routes: BusGatewayRoute[]; } -// ——— Settings ——— +// --- Settings --- export type SettingValueKind = "string" | "number" | "boolean" | "color"; /** * How a row behaves: - * - `editable` — stored in the database, changeable here, takes effect immediately. - * - `readonly` — an environment value, shown so you can see what this instance runs + * - `editable` - stored in the database, changeable here, takes effect immediately. + * - `readonly` - an environment value, shown so you can see what this instance runs * on. Read once at startup, so there's nothing to change here. - * - `presence` — an environment value whose content stays on the server; only + * - `presence` - an environment value whose content stays on the server; only * whether it's set is reported. */ export type SettingAccess = "editable" | "readonly" | "presence"; /** * One setting on the settings page. Only settings that take effect immediately are - * editable, so there is no "restart required" state to represent — anything read + * editable, so there is no "restart required" state to represent - anything read * once at startup appears as `readonly` or `presence` instead. */ export interface SettingRow { @@ -1057,22 +1245,22 @@ export interface SettingRow { kind: SettingValueKind; /** The product default a reset returns to. Stored as a string per `kind`; empty for secrets. */ defaultValue: string; - /** The stored value, or null for a secret — those never leave the server. */ + /** The stored value, or null for a secret - those never leave the server. */ value: string | null; secret: boolean; /** The stored value differs from the product default, so a reset would do something. */ overridden: boolean; - /** Whether the effective value is non-empty — the only way a secret reveals it is set. */ + /** Whether the effective value is non-empty - the only way a secret reveals it is set. */ hasValue: boolean; /** * Whether this row can be written. False for every environment value, and for a secret on an - * instance with no encryption key configured — there's nowhere safe to store that one. + * instance with no encryption key configured - there's nowhere safe to store that one. */ editable: boolean; access: SettingAccess; } -// ——— Retention ——— +// --- Retention --- /** Retention settings to preview before saving; anything left out keeps its current value. */ export interface RetentionProposal { @@ -1129,7 +1317,7 @@ export interface AggregationRetentionCheck { warning: string | null; } -// ——— Exchanges ——— +// --- Exchanges --- /** * The four observable outcomes of an exchange. "badResponse" = the handler @@ -1145,7 +1333,7 @@ export interface ExchangeFileRef { key: string | null; } -/** One exchange as the Exchanges page sees it — names pre-resolved for display. */ +/** One exchange as the Exchanges page sees it - names pre-resolved for display. */ export interface ExchangeRow { id: string; status: ExchangeStatus; @@ -1163,7 +1351,7 @@ export interface ExchangeRow { retryFor: string | null; /** * True when a retry has already been made from this exchange. An exchange gets at most one - * retry, so this is also what makes it un-retryable — the newest attempt in the chain is the + * retry, so this is also what makes it un-retryable - the newest attempt in the chain is the * one to act on. */ hasRetry: boolean; @@ -1180,7 +1368,7 @@ export interface ExchangeRow { sourceXchangeId: string | null; /** What it read from the input of `sourceXchangeId`, the original document, by path. */ sourceValues: Record | null; - /** True when the subscription has no mapper — the Mapped stage is skipped. */ + /** True when the subscription has no mapper - the Mapped stage is skipped. */ mapperSkipped: boolean; files: { input: ExchangeFileRef | null; @@ -1194,7 +1382,7 @@ export interface RetryTreeNode { id: string; /** null on the original attempt. */ retryFor: string | null; - /** What the payload promoted — how an attempt is named, the same as in the exchange list. */ + /** What the payload promoted - how an attempt is named, the same as in the exchange list. */ promotedProperties: Record | null; startedOn: string; finishedOn: string | null; @@ -1230,7 +1418,7 @@ export interface RetrySubstitution { } export interface RetrySkip { - /** The exchange the skip is about — the one selected, or the attempt that stood in for it. */ + /** The exchange the skip is about - the one selected, or the attempt that stood in for it. */ id: string; reason: string; } @@ -1269,7 +1457,7 @@ export interface ExchangeQuery { ids?: string; /** * The exchanges one run of a scheduled job created. The server reads the run's own list of - * them, so this says the same thing as spelling every id into `ids` — at any size, and + * them, so this says the same thing as spelling every id into `ids` - at any size, and * without putting them all in the URL. */ receiveAttemptId?: number; @@ -1286,18 +1474,48 @@ export interface ExchangeQuery { * all", which is worth being able to ask. */ propertyKey?: string; + /** Case-insensitive substring of the exception text. */ + errorContains?: string; + /** Only exchanges that are a retry of another one. */ + isRetry?: boolean; + /** Only exchanges with an automatic retry booked. */ + scheduledRetry?: boolean; + /** Only exchanges rolled up into an aggregation. */ + aggregated?: boolean; from?: string; to?: string; offset: number; limit: number; } +/** One slice of the exchange volume histogram: how many started in it, by status. */ +export interface ExchangeVolumeBucket { + /** UTC start of the slice. */ + start: string; + processing: number; + success: number; + badResponse: number; + failed: number; +} + +/** + * How the exchanges a filter matches break down: a count per status, ignoring any status filter, + * and, when the filter has a start time, how they spread over that range. + */ +export interface ExchangeBreakdown { + /** Each capped like the list total: a value past the cap means "more than". */ + counts: Record; + buckets: ExchangeVolumeBucket[]; + /** The width of one bucket; 0 when there are none. */ + bucketMinutes: number; +} + export interface Paged { result: T[]; total: number; } -// ——— Scheduled retries ——— +// --- Scheduled retries --- /** A failed exchange whose retry policy scheduled an automatic retry. */ export interface ScheduledRetryRow { @@ -1312,12 +1530,12 @@ export interface ScheduledRetryRow { exception: string | null; /** When the failed exchange originally started. */ startedOn: string; - /** What the exchange carries — how a pending retry identifies itself in a list. */ + /** What the exchange carries - how a pending retry identifies itself in a list. */ promotedProperties: Record | null; /** * The shared retry policy the subscription currently points at. Null when the - * policy is defined inline on the subscription instead, so the subscription — not - * the Retry policies list — is where to go and look. + * policy is defined inline on the subscription instead, so the subscription - not + * the Retry policies list - is where to go and look. */ retryPolicyId: number | null; retryPolicyName: string | null; @@ -1334,7 +1552,7 @@ export interface ScheduledRetryQuery { limit: number; } -// ——— Queue health (Ops) ——— +// --- Queue health (Ops) --- export type QueueSeverity = "healthy" | "warning" | "critical"; @@ -1353,7 +1571,7 @@ export interface QueueHealthSummary { /** * What a queue is for. Resolved by `Ops/LaneResolver`, which sits next to - * `WorkGroup.GetBusMessageName()` — the formula that builds the name in the + * `WorkGroup.GetBusMessageName()` - the formula that builds the name in the * first place. The wording for each lane is this app's, in `QueueHealthPage`. */ export type QueueLane = "FrontDoor" | "Work" | "Notifications" | "Legacy" | "Control"; @@ -1406,7 +1624,7 @@ export interface DeadLetterRow { /** * A queue RabbitMQ has that nothing here reads: a paused information type's, or one left * behind by an older version of Bitween. Every other view on this page is built from what - * the running process declares — so these are invisible everywhere else. + * the running process declares - so these are invisible everywhere else. */ export interface UnattendedQueue { queueName: string; @@ -1425,6 +1643,18 @@ export interface UnattendedQueue { } /** What a delete of unattended lanes did: a lane that couldn't go is skipped, not fatal. */ +/** One message in a dead-letter queue, read without taking it off. */ +export interface DeadLetterMessage { + position: number; + body: string; + /** The body was longer than the server sends, and this is its start. */ + bodyCut: boolean; + lastException: string | null; + correlationId: string | null; + routingKey: string | null; + exceptionHistory: string[]; +} + export interface UnattendedDeleteResult { deleted: string[]; skipped: { queueName: string; reason: string }[]; @@ -1448,11 +1678,11 @@ export interface QueueHealthSnapshot { alerts: QueueAlert[]; } -// ——— Dashboard ——— +// --- Dashboard --- /** One chain that has been retried repeatedly and is still failing. */ export interface FailingChain { - /** The newest attempt — where this chain got to, and the one a retry continues from. */ + /** The newest attempt - where this chain got to, and the one a retry continues from. */ id: string; /** How many attempts it has taken, counting the original as one. */ attempts: number; @@ -1478,17 +1708,11 @@ export interface DashboardData { * value above it means "at least this many". */ failuresToActOn: number; - today: { total: number; failed: number; processing: number }; - yesterdayTotal: number; - /** Percentage 0–100 across the last 7 days of finished exchanges. */ - successRate7d: number; + /** Exchanges started in the last 24 hours and not finished yet. */ + inFlight: number; pendingRetries: number; - /** Null when the RabbitMQ management API is unavailable — never counts as zero. */ + /** Null when the RabbitMQ management API is unavailable - never counts as zero. */ queueAlerts: number | null; - /** Last 14 days, oldest first; today is the final entry. */ - trafficByDay: { date: string; success: number; failed: number }[]; - /** Top subscriptions by 7-day traffic, busiest first. */ - busiest: { id: number; name: string; count: number; failed: number }[]; latestFailures: { id: string; status: ExchangeStatus; @@ -1505,12 +1729,12 @@ export interface DashboardData { } /** - * One connection setting, as the adapter itself declares it. The UI keeps no list of its own — + * One connection setting, as the adapter itself declares it. The UI keeps no list of its own - * see DataSourceProviderCatalog on the server for why. */ export interface DataSourceProviderSetting { name: string; - /** string, number or boolean — enough to pick an input, nothing more. */ + /** string, number or boolean - enough to pick an input, nothing more. */ type: "string" | "number" | "boolean"; hint: string | null; /** What a new data source starts with. Null means start it empty. */ @@ -1526,10 +1750,99 @@ export interface DataSourceProvider { label: string; /** * What it connects to: Broker, Relational, Document, ObjectStore or Http. A bus gateway can - * only read from a Broker — a provider that holds a database session is a data source too, but + * only read from a Broker - a provider that holds a database session is a data source too, but * it has no queue to consume. */ kind: string; description: string | null; settings: DataSourceProviderSetting[]; } + +/** GET /settings/about: this Bitween and how it is set up, as the node that answered sees it. */ +export interface InstanceAbout { + version: string; + build: string | null; + node: { name: string; startedOn: string; runtime: string; os: string; dataSources: boolean }; + database: string; + storage: { provider: string; adapterPath: string; documentPrefix: string }; + broker: { queuePrefix: string; managementConfigured: boolean }; + health: { name: string; status: string; durationMs: number; description: string | null; error: string | null }[]; + adapters: { + runtimes: { name: string; available: boolean; version: string | null; reason: string | null }[]; + pip: { available: boolean; version: string | null; reason: string | null }; + npm: { available: boolean; version: string | null; reason: string | null }; + commandTimeoutSeconds: number; + editor: { dependencies: boolean; memoryMb: number; cpuCores: number }; + }; + limits: { + signInPerMinute: number; + requestsPerMinute: number; + fileLinksPerMinute: number; + maxResponseWaitSeconds: number; + maxRetryChainDepth: number; + staleRunAfterMinutes: number; + notifierQuietMinutes: number; + }; + network: { publicUrl: string | null; blockPrivateNetworkAddresses: boolean; trustedProxies: number; exposeApiDocs: boolean; corsOrigins: string[] }; + retention: { receiveAttemptRetentionDays: number; receiveAttemptCleanupCron: string; inboundMessagePruneCron: string }; + telemetry: { openTelemetry: boolean }; +} + +/** GET /cluster/nodes: the nodes, as their heartbeats describe them. */ +export interface ClusterNodes { + answeredBy: string; + goneAfterSeconds: number; + nodes: { + name: string; + host: string; + startedOn: string; + lastSeenOn: string; + online: boolean; + version: string; + dataSources: boolean; + runtimes: string[]; + leases: { resource: string; term: number; acquiredOn: string }[]; + holdsDataSources: { id: number; name: string }[]; + }[]; +} + +/** POST /adapters/packages: what was published. */ +export interface AdapterUploadResult { + adapterId: string; + version: string; + current: boolean; + sha256: string; +} + +/** GET /apigateways/{id}/rejections: calls to the gateway turned away, on the node that answered. */ +export interface GatewayRejections { + node: string; + since: string; + counts: Record; + /** Every node's refusals over the last week, by reason. Absent from servers before metrics. */ + lastSevenDays?: Record; + recent: { on: string; reason: string; status: number; address: string | null }[]; +} + +/** GET /ops/background: Bitween's own system jobs, and the outbox to the broker. */ +export interface BackgroundWork { + jobs: { + name: string; + does: string; + cron: string | null; + scheduled: boolean; + state: "Normal" | "Paused" | "Error" | "NotScheduled"; + lastRanOn: string | null; + nextRunOn: string | null; + runningNow: boolean; + }[]; + outbox: { pending: number; oldestPendingOn: string | null; failing: number; lastError: string | null; publishedLastHour: number }; +} + +/** A label subscriptions are filed under, to find them by. */ +export interface SubscriptionCategory { + id: number; + code: string; + description: string | null; + createdOn: string; +} diff --git a/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx b/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx index 5a479a6f..6a599db1 100644 --- a/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx +++ b/SW.Bitween.Web/ClientApp/src/auth/SessionContext.tsx @@ -1,18 +1,10 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, - type ReactNode, -} from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { api, TOKEN_KEY, onSessionEnded, type PermissionKey, type Session } from "../api"; +import { api, onSessionEnded, onSignedOutElsewhere, type PermissionKey, type Session } from "../api"; import { useIdleLogout } from "./useIdleLogout"; +import { SessionContext } from "./useSession"; -interface SessionContextValue { +export interface SessionContextValue { session: Session | null; /** True until the stored session has been checked once at startup. */ initializing: boolean; @@ -24,9 +16,11 @@ interface SessionContextValue { /** Re-fetch the session after profile changes. */ refresh: () => Promise; signOut: () => Promise; + /** Changes the password and signs back in with it, so this tab carries on. */ + changePassword: (currentPassword: string, newPassword: string) => Promise; /** * Set when the server could not be asked whether this browser's token is still good - * — rate limited, down, or unreachable. Distinct from `session === null`, which + * - rate limited, down, or unreachable. Distinct from `session === null`, which * means it answered and the answer was no. */ unreachable: boolean; @@ -34,8 +28,6 @@ interface SessionContextValue { retry: () => void; } -const SessionContext = createContext(null); - export function SessionProvider({ children }: { children: ReactNode }) { const [session, setSession] = useState(null); const [initializing, setInitializing] = useState(true); @@ -44,7 +36,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { * Bumped whenever the session is authoritatively replaced or ended. * * `getSession()` cannot be cancelled, so a slow one can resolve *after* a - * sign-out and hand `setSession` the very session that was just discarded — + * sign-out and hand `setSession` the very session that was just discarded - * signing the user back in. Each caller notes the generation it started under * and drops its own result if that is no longer current. */ @@ -70,7 +62,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { .catch(() => { // Not "signed out": `getSession` only resolves to null when the server said so. // Reaching here means it could not be asked, and the token this browser holds - // may well still be good — so keep it and say the server is unreachable rather + // may well still be good - so keep it and say the server is unreachable rather // than sending someone to sign in again for a blip. if (generation.current === startedAt) setUnreachable(true); }) @@ -117,7 +109,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { const signOut = useCallback(async () => { // The session ends here, whatever the server says. `await api.logout()` used to - // come first, so any failure — a backend restart, a blip, an unreachable host — + // come first, so any failure - a backend restart, a blip, an unreachable host - // threw before the two lines below ran: signed out everywhere except the one // place that decides what you see, which then needed a manual refresh. Ending it // first also means the login page appears immediately rather than after the @@ -126,25 +118,39 @@ export function SessionProvider({ children }: { children: ReactNode }) { await api.logout(); }, [endSession]); + /** + * Changing a password ends every session the account holds, this one included: the server + * deletes its refresh tokens, and a token issued while the password still had to change grants + * nothing even after it has. Signing straight back in with the new password is what lets this + * tab carry on - otherwise the forced change left a working-looking app with no permissions, + * and an ordinary change signed you out whenever the current token expired. + */ + const changePassword = useCallback( + async (currentPassword: string, newPassword: string) => { + const email = session?.user.email ?? ""; + await api.changePassword(currentPassword, newPassword); + try { + await signIn(email, newPassword); + } catch { + // The password did change; only the new session failed. Signing out sends them to sign + // in with it, rather than leaving them in the session that just stopped counting. + await signOut(); + } + }, + [session, signIn, signOut], + ); + /** * Ends the session here when it ends in another tab. * - * Both credentials are shared by every tab of this origin — the Jwt in - * localStorage and the refresh cookie — so a sign-out in one tab really has ended - * the session in all of them; the others simply never found out and kept rendering - * the whole app over reads that could only fail. `storage` fires only in the - * *other* tabs, which is exactly the audience. A null key means the entire store - * was cleared, which the logout response's `Clear-Site-Data` does, so that counts - * too; a key with a new value is another tab signing *in*, which doesn't. + * The refresh cookie is shared by every tab of this origin, so a sign-out in one + * tab really has ended the session in all of them; the others simply never found + * out and kept rendering the whole app over reads that could only fail. The tab + * that signs out says so over a BroadcastChannel, which reaches only the others. */ useEffect(() => { - const onStorage = (e: StorageEvent) => { - if (e.key !== null && e.key !== TOKEN_KEY) return; - if (e.newValue) return; - endSession(); - }; - window.addEventListener("storage", onStorage); - return () => window.removeEventListener("storage", onStorage); + onSignedOutElsewhere(endSession); + return () => onSignedOutElsewhere(null); }, [endSession]); /** @@ -152,7 +158,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { * * Covers what no `storage` event can: a Jwt and refresh cookie that both expired * while a tab sat open, or an administrator disabling the account. `request()` - * already knew — it writes "Your session has ended" — but it could only throw that + * already knew - it writes "Your session has ended" - but it could only throw that * at whichever page happened to be asking, and a page's error state cannot sign * anyone out. */ @@ -175,6 +181,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { adoptSession, refresh, signOut, + changePassword, unreachable, retry, }), @@ -186,6 +193,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { adoptSession, refresh, signOut, + changePassword, unreachable, retry, ], @@ -193,9 +201,3 @@ export function SessionProvider({ children }: { children: ReactNode }) { return {children}; } - -export function useSession(): SessionContextValue { - const ctx = useContext(SessionContext); - if (!ctx) throw new Error("useSession must be used inside "); - return ctx; -} diff --git a/SW.Bitween.Web/ClientApp/src/auth/__tests__/SessionOutage.test.tsx b/SW.Bitween.Web/ClientApp/src/auth/__tests__/SessionOutage.test.tsx index 2c24b136..4c6c114c 100644 --- a/SW.Bitween.Web/ClientApp/src/auth/__tests__/SessionOutage.test.tsx +++ b/SW.Bitween.Web/ClientApp/src/auth/__tests__/SessionOutage.test.tsx @@ -1,14 +1,14 @@ import { screen, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { describe, expect, it } from "vitest"; -import { TOKEN_KEY } from "../../api"; +import { getToken } from "../../api/http/request"; import { apiPath, renderApp } from "../../__tests__/support/renderApp"; /** * A server that cannot answer is not a server saying "signed out". * * `getSession` used to swallow every failure and return null, which the guard reads as - * "not signed in" — so a rate-limited or briefly unreachable backend threw people to + * "not signed in" - so a rate-limited or briefly unreachable backend threw people to * the sign-in page mid-task, with a perfectly good token still in localStorage. That is * also what made a whole afternoon of rate-limited test runs look like an auth problem. */ @@ -30,12 +30,12 @@ describe("a session read the server could not answer", () => { // The distinction that matters: still signed in, so nothing asks for a password // and the token is left where it is. expect(router.state.location.pathname).toBe("/subscriptions"); - expect(localStorage.getItem(TOKEN_KEY)).toBeTruthy(); + expect(getToken()).toBeTruthy(); }); it("still signs you out on a 401, because that one is the server's answer", async () => { // Both the profile read and the silent refresh behind it, so there is nothing left - // to restore the session with — which is a real, unrecoverable sign-out. + // to restore the session with - which is a real, unrecoverable sign-out. const { router } = renderApp("/subscriptions", { handlers: [ http.get(apiPath("/accounts/profile"), () => new HttpResponse(null, { status: 401 })), diff --git a/SW.Bitween.Web/ClientApp/src/auth/__tests__/SignOut.test.tsx b/SW.Bitween.Web/ClientApp/src/auth/__tests__/SignOut.test.tsx index 2b6337ee..517eecb5 100644 --- a/SW.Bitween.Web/ClientApp/src/auth/__tests__/SignOut.test.tsx +++ b/SW.Bitween.Web/ClientApp/src/auth/__tests__/SignOut.test.tsx @@ -2,22 +2,22 @@ import { screen, waitFor } from "@testing-library/react"; import type { UserEvent } from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { describe, expect, it } from "vitest"; -import { TOKEN_KEY } from "../../api"; +import { getToken, setToken } from "../../api/http/request"; import { apiPath, renderApp } from "../../__tests__/support/renderApp"; import { server } from "../../__tests__/support/server"; /** * Signing out, and the ways it used to go wrong. * - * The session lives in four places — a row in the database, the HttpOnly refresh + * The session lives in four places - a row in the database, the HttpOnly refresh * cookie, the Jwt in localStorage, and React's own copy. Only the last one decides * what you see, and it used to be the one thing a sign-out could fail to clear: * `signOut` awaited the server first, so any failure threw before the session was * ended and left the whole app on screen with the Jwt already deleted. Every test * here is a way that could happen. * - * The one that needs two real tabs — a sign-out in one ending the session in the - * other — stays in e2e/sign-out.spec.ts. + * The one that needs two real tabs - a sign-out in one ending the session in the + * other - stays in e2e/sign-out.spec.ts. */ /** Somewhere every account can be, which asks the API for nothing but the permission catalog. */ @@ -46,7 +46,7 @@ const acceptSignIn = () => server.use(http.post(apiPath("/accounts/login"), () => HttpResponse.json({ jwt: "fresh-token" }))); /** - * A logout the test answers when it chooses — or never. Records the request, so a test can + * A logout the test answers when it chooses - or never. Records the request, so a test can * see it went out, and whether it was cancelled while waiting. */ function heldLogout(answer: (request: Request) => Response = () => HttpResponse.json({})) { @@ -93,7 +93,7 @@ describe("signing out", () => { expect(await loginPage()).toBeVisible(); await waitFor(() => expect(answered).toBe(true)); expect(router.state.location.pathname).toBe("/login"); - expect(localStorage.getItem(TOKEN_KEY)).toBeNull(); + expect(getToken()).toBeNull(); }); it("still signs you out when the backend is unreachable", async () => { @@ -132,7 +132,7 @@ describe("signing out", () => { it("cannot let a slow sign-out response wipe the session that replaced it", async () => { // The logout response carries `Clear-Site-Data: "cache", "cookies", "storage"`, which - // the browser applies to the whole origin whenever it lands — including over a newer + // the browser applies to the whole origin whenever it lands - including over a newer // sign-in. Cancelling the request means the response never arrives. jsdom ignores the // header, so the mock does what the browser would, and only if the response reaches it. const logout = heldLogout((request) => { @@ -150,13 +150,13 @@ describe("signing out", () => { await waitFor(() => expect(logout.landed).toBe(true)); expect(router.state.location.pathname).not.toBe("/login"); - expect(localStorage.getItem(TOKEN_KEY)).toBe("fresh-token"); + expect(getToken()).toBe("fresh-token"); expect(shell()).toBeInTheDocument(); }); it("cannot let a slow session read flash the app back after a sign-out", async () => { // Held open so it lands after the sign-out below. It then falls through to the ordinary - // 200 — the Jwt went out before the sign-out and is still valid — so its result must be + // 200 - the Jwt went out before the sign-out and is still valid - so its result must be // discarded on arrival rather than trusted, or the app appears again over a dead session. let profileRequested = false; let release!: () => void; @@ -180,12 +180,12 @@ describe("signing out", () => { watcher.observe(document.body, { childList: true, subtree: true }); await waitFor(() => expect(profileRequested).toBe(true)); - // A sign-out in another tab, as this one hears it: the key gone, and a `storage` event. + // A sign-out in another tab, as this one hears it: a message on the session channel. // Real delivery between two tabs is what the e2e spec's two-tab test covers. - localStorage.removeItem(TOKEN_KEY); - window.dispatchEvent( - new StorageEvent("storage", { key: TOKEN_KEY, oldValue: "test-token", newValue: null, storageArea: localStorage }), - ); + const otherTab = new BroadcastChannel("bitween-session"); + otherTab.postMessage({ type: "signed-out" }); + otherTab.close(); + await waitFor(() => expect(getToken()).toBeNull()); release(); // Settled one way or the other, now that the read has landed. @@ -201,7 +201,7 @@ describe("signing out", () => { await screen.findByRole("button", { name: "Account menu" }); // Exactly what a sign-out elsewhere leaves behind: no cookie, a useless Jwt. - localStorage.setItem(TOKEN_KEY, "dead"); + setToken("dead"); const refused = () => new HttpResponse(null, { status: 401 }); server.use( // Everything the partners page reads. diff --git a/SW.Bitween.Web/ClientApp/src/auth/__tests__/TokenStorage.test.tsx b/SW.Bitween.Web/ClientApp/src/auth/__tests__/TokenStorage.test.tsx new file mode 100644 index 00000000..e78f1324 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/auth/__tests__/TokenStorage.test.tsx @@ -0,0 +1,65 @@ +import { screen } from "@testing-library/react"; +import { http, HttpResponse } from "msw"; +import { describe, expect, it } from "vitest"; +import { getToken, SIGNED_IN_KEY, TOKEN_KEY } from "../../api/http/request"; +import { apiPath, profile, renderApp } from "../../__tests__/support/renderApp"; + +/** + * The Jwt lives in memory, not in localStorage, where any script that ever ran on the page could + * read it. A reload therefore starts without one and gets it back from the HttpOnly refresh + * cookie - but only when this browser held a session, so the sign-in page doesn't spend the + * sign-in rate limit probing for one. + */ +const empty = () => HttpResponse.json({ result: [], totalCount: 0 }); +const partnersPage = ["/partners", "/subscriptions", "/apigateways", "/busgateways"].map((path) => + http.get(apiPath(path), empty), +); + +describe("where the Jwt is kept", () => { + it("is never written to localStorage", async () => { + renderApp("/partners", { handlers: partnersPage }); + await screen.findByRole("button", { name: "Account menu" }); + + expect(getToken()).toBe("test-token"); + expect(localStorage.getItem(TOKEN_KEY)).toBeNull(); + for (let i = 0; i < localStorage.length; i++) + expect(localStorage.getItem(localStorage.key(i)!)).not.toContain("test-token"); + }); + + it("comes back from the refresh cookie after a reload", async () => { + // A reload: no Jwt in memory, but this browser was signed in. + localStorage.setItem(SIGNED_IN_KEY, "1"); + let refreshed = false; + renderApp("/partners", { + as: null, + handlers: [ + ...partnersPage, + profile(), + http.post(apiPath("/accounts/login"), () => { + refreshed = true; + return HttpResponse.json({ jwt: "from-cookie" }); + }), + ], + }); + + expect(await screen.findByRole("button", { name: "Account menu" })).toBeVisible(); + expect(refreshed).toBe(true); + expect(getToken()).toBe("from-cookie"); + }); + + it("does not probe for a session on a first visit", async () => { + let probed = false; + renderApp("/partners", { + as: null, + handlers: [ + http.post(apiPath("/accounts/login"), () => { + probed = true; + return new HttpResponse(null, { status: 400 }); + }), + ], + }); + + expect(await screen.findByRole("heading", { name: "Sign in" })).toBeVisible(); + expect(probed).toBe(false); + }); +}); diff --git a/SW.Bitween.Web/ClientApp/src/auth/guards.tsx b/SW.Bitween.Web/ClientApp/src/auth/guards.tsx index bacfbb78..70f2d739 100644 --- a/SW.Bitween.Web/ClientApp/src/auth/guards.tsx +++ b/SW.Bitween.Web/ClientApp/src/auth/guards.tsx @@ -4,7 +4,7 @@ import { CloudOff, Lock } from "lucide-react"; import type { PermissionKey } from "../api"; import { labelIn, usePermissionCatalog } from "../api/permissions"; import { Button } from "../components/ui/basics"; -import { useSession } from "./SessionContext"; +import { useSession } from "./useSession"; /** Redirects to /login when signed out; shows a splash while checking. */ export function RequireAuth() { @@ -18,18 +18,18 @@ export function RequireAuth() { ); } - // Nobody said this session was over — the server could not be asked. Sending someone + // Nobody said this session was over - the server could not be asked. Sending someone // to sign in here would be a lie, and one that loses the page they were on and invites // them to re-enter a password they never needed to. if (unreachable) { return (
- +

Can't reach Bitween

- You are still signed in — the server just didn't answer. This usually clears on its own. + You are still signed in - the server just didn't answer. This usually clears on its own.

@@ -48,11 +48,11 @@ export function RequireAuth() { /** Full-page stop for routes the session's roles don't unlock. */ export function AccessDenied({ permission }: { permission: PermissionKey }) { - // Falls back to the raw key until the catalog lands — never blocks the message. + // Falls back to the raw key until the catalog lands - never blocks the message. const label = labelIn(usePermissionCatalog().data ?? [], permission); return (
- +

You don't have access to this page

@@ -83,9 +83,3 @@ export function Can({ permission, children }: { permission: PermissionKey; child if (!can(permission)) return null; return <>{children}; } - -/** Convenience hook for components that branch on a permission. */ -export function useSessionCan(permission: PermissionKey): boolean { - const { can } = useSession(); - return can(permission); -} diff --git a/SW.Bitween.Web/ClientApp/src/auth/useSession.ts b/SW.Bitween.Web/ClientApp/src/auth/useSession.ts new file mode 100644 index 00000000..62885a87 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/auth/useSession.ts @@ -0,0 +1,10 @@ +import { createContext, useContext } from "react"; +import type { SessionContextValue } from "./SessionContext"; + +export const SessionContext = createContext(null); + +export function useSession(): SessionContextValue { + const ctx = useContext(SessionContext); + if (!ctx) throw new Error("useSession must be used inside "); + return ctx; +} diff --git a/SW.Bitween.Web/ClientApp/src/auth/useSessionCan.ts b/SW.Bitween.Web/ClientApp/src/auth/useSessionCan.ts new file mode 100644 index 00000000..422b4665 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/auth/useSessionCan.ts @@ -0,0 +1,8 @@ +import type { PermissionKey } from "../api"; +import { useSession } from "./useSession"; + +/** Convenience hook for components that branch on a permission. */ +export function useSessionCan(permission: PermissionKey): boolean { + const { can } = useSession(); + return can(permission); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/charts/Lines.tsx b/SW.Bitween.Web/ClientApp/src/components/charts/Lines.tsx new file mode 100644 index 00000000..16387c4f --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/charts/Lines.tsx @@ -0,0 +1,172 @@ +import { useRef, useState } from "react"; +import { compact } from "../../lib/numbers"; +import { niceMax } from "./scale"; + +export interface LineSeries { + key: string; + label: string; + /** The line's color, as a hex the stroke takes. */ + color: string; +} + +/** + * Lines over time on one axis, with a crosshair that snaps to the nearest point and lists every + * series there. 2px lines, a legend for two or more series, and a table of the same numbers. + */ +export function Lines({ + points, + series, + height = 140, + label, + tick, + format = compact, +}: { + points: { at: string; values: Record }[]; + series: LineSeries[]; + height?: number; + label: (at: string) => string; + /** Up to five evenly spaced axis labels are shown. */ + tick: (at: string) => string; + format?: (n: number) => string; +}) { + const box = useRef(null); + const [hover, setHover] = useState(null); + const max = niceMax(Math.max(1, ...points.flatMap((p) => series.map((s) => p.values[s.key] ?? 0)))); + const n = Math.max(1, points.length - 1); + const x = (i: number) => (i / n) * 1000; + const y = (v: number) => height - (v / max) * height; + const ticks = points.length <= 1 ? [0] : [0, 0.25, 0.5, 0.75, 1].map((f) => Math.round(f * n)); + + const onMove = (e: React.PointerEvent) => { + const r = box.current?.getBoundingClientRect(); + if (!r || points.length === 0) return; + setHover(Math.max(0, Math.min(points.length - 1, Math.round(((e.clientX - r.left) / r.width) * n)))); + }; + + return ( +
+ {series.length >= 2 && ( +
+ {series.map((s) => ( + + + {s.label} + + ))} +
+ )} +
+ {[1, 0.5].map((f) => ( +
+ + {format(max * f)} + +
+ ))} +
setHover(null)} + style={{ height }} + > +
+ + {series.map((s) => ( + `${x(i)},${y(p.values[s.key] ?? 0)}`).join(" ")} + /> + ))} + {hover !== null && ( + + )} + + {hover !== null && points[hover] && ( +
0.6 + ? { right: `${100 - (hover / n) * 100}%`, marginRight: 8 } + : { left: `${(hover / n) * 100}%`, marginLeft: 8 } + } + > +

{label(points[hover].at)}

+ {series.map((s) => ( +

+ + + {(points[hover].values[s.key] ?? 0).toLocaleString("en")} + + {s.label.toLowerCase()} +

+ ))} +
+ )} +
+
+ {ticks.map((i) => + points[i] ? ( + + {tick(points[i].at)} + + ) : null, + )} +
+
+
+ View as table +
+ + + + + {series.map((s) => ( + + ))} + + + + {points.map((p) => ( + + + {series.map((s) => ( + + ))} + + ))} + +
When + {s.label} +
{label(p.at)} + {(p.values[s.key] ?? 0).toLocaleString("en")} +
+
+
+
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/charts/Sparkline.tsx b/SW.Bitween.Web/ClientApp/src/components/charts/Sparkline.tsx new file mode 100644 index 00000000..4ce053f3 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/charts/Sparkline.tsx @@ -0,0 +1,35 @@ +/** + * A trend in a table cell: a line in a quiet ink, the latest point in the accent. Read with the + * number beside it; the label says what it shows for a screen reader. + */ +export function Sparkline({ + values, + label, + width = 72, + height = 20, +}: { + values: number[]; + label: string; + width?: number; + height?: number; +}) { + if (values.length === 0) return null; + const max = Math.max(1, ...values); + const n = Math.max(1, values.length - 1); + const pts = values.map((v, i) => [(i / n) * (width - 4) + 2, height - 2 - (v / max) * (height - 4)] as const); + const [lx, ly] = pts[pts.length - 1]; + return ( + + {label} + `${px},${py}`).join(" ")} + /> + + + ); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/charts/TimeBars.tsx b/SW.Bitween.Web/ClientApp/src/components/charts/TimeBars.tsx new file mode 100644 index 00000000..3695559f --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/charts/TimeBars.tsx @@ -0,0 +1,193 @@ +import { useState, type ReactNode } from "react"; +import { compact } from "../../lib/numbers"; +import { niceMax } from "./scale"; + +export interface BarSeries { + key: string; + label: string; + /** A background class: the mark's color. Text never wears it. */ + className: string; +} + +export interface TimePoint { + at: string; + values: Record; +} + +/** + * Stacked columns over time, one series per outcome, with an optional comparison line (the same + * hours a week ago). Thin columns with rounded ends and a 2px gap between segments; a tooltip + * per column on hover; a legend for two or more series; and the same numbers as a table. + */ +export function TimeBars({ + points, + series, + compare, + height = 140, + label, + tick, + every = 1, + format = compact, +}: { + points: TimePoint[]; + series: BarSeries[]; + /** A line over the columns, in a neutral ink, e.g. last week's totals at the same positions. */ + compare?: { label: string; values: (number | null)[] }; + height?: number; + /** For the table and the tooltip: "Oct 9, 14:00". */ + label: (at: string) => string; + /** The axis label under a column, or nothing. */ + tick: (at: string, index: number) => ReactNode; + /** Label every nth column. */ + every?: number; + format?: (n: number) => string; +}) { + const [hover, setHover] = useState(null); + const totals = points.map((p) => series.reduce((n, s) => n + (p.values[s.key] ?? 0), 0)); + const max = niceMax(Math.max(1, ...totals, ...(compare?.values.map((v) => v ?? 0) ?? []))); + const y = (v: number) => (v / max) * height; + const legend = [ + ...series.map((s) => ({ + key: s.key, + label: s.label, + mark: , + })), + ]; + if (compare) + legend.push({ key: "compare", label: compare.label, mark: }); + + return ( +
+ {legend.length >= 2 && ( +
+ {legend.map((l) => ( + + + {l.mark} + + {l.label} + + ))} +
+ )} +
setHover(null)}> + {[1, 0.5].map((f) => ( +
+ + {format(max * f)} + +
+ ))} +
+
+ {points.map((p, i) => ( +
setHover(i)} + > + {hover === i && ( +
+

{label(p.at)}

+ {series.map((s) => ( +

+ + {(p.values[s.key] ?? 0).toLocaleString("en")} + {s.label.toLowerCase()} +

+ ))} + {compare && compare.values[i] !== null && ( +

+ + {(compare.values[i] ?? 0).toLocaleString("en")} + {compare.label.toLowerCase()} +

+ )} +
+ )} +
+ {series.map((s, k) => { + const v = p.values[s.key] ?? 0; + if (v <= 0) return null; + // The data end is rounded; the baseline end stays square. + const top = !series.slice(k + 1).some((above) => (p.values[above.key] ?? 0) > 0); + return ( +
+ ); + })} + {totals[i] === 0 &&
} +
+ + {i % every === 0 ? tick(p.at, i) : ""} + +
+ ))} +
+ {compare && ( + + (v === null ? null : `${i + 0.5},${height - y(v)}`)) + .filter(Boolean) + .join(" ")} + /> + + )} +
+
+ View as table +
+ + + + + {series.map((s) => ( + + ))} + {compare && } + + + + {points.map((p, i) => ( + + + {series.map((s) => ( + + ))} + {compare && } + + ))} + +
When + {s.label} + {compare.label}
{label(p.at)} + {(p.values[s.key] ?? 0).toLocaleString("en")} + {compare.values[i] ?? "-"}
+
+
+
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/charts/scale.ts b/SW.Bitween.Web/ClientApp/src/components/charts/scale.ts new file mode 100644 index 00000000..e5fc3d3a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/charts/scale.ts @@ -0,0 +1,7 @@ +/** The top of an axis: the next round number (1, 2 or 5 times a power of ten) at or above the largest value. */ +export function niceMax(max: number): number { + if (max <= 0) return 1; + const power = 10 ** Math.floor(Math.log10(max)); + for (const step of [1, 2, 5, 10]) if (step * power >= max) return step * power; + return 10 * power; +} diff --git a/SW.Bitween.Web/ClientApp/src/components/code/CodeEditor.tsx b/SW.Bitween.Web/ClientApp/src/components/code/CodeEditor.tsx new file mode 100644 index 00000000..15fdcaa8 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/code/CodeEditor.tsx @@ -0,0 +1,198 @@ +import { useEffect, useRef } from "react"; +import { languageOf, monaco } from "./monacoSetup"; +import type { EditorProblem } from "./problems"; + +export interface CodeEditorProps { + /** Keeps two drafts' files apart in Monaco's shared model registry. */ + scope: string; + files: Record; + /** The file shown; null shows nothing. */ + active: string | null; + theme: "dark" | "light"; + /** What the last Check said about particular lines, drawn as squiggles in the code. */ + checkProblems: EditorProblem[]; + /** A request to put the cursor somewhere - a click in Problems. `seq` makes a repeat count. */ + revealAt: { path: string; line: number; column: number; seq: number } | null; + onChange: (path: string, text: string) => void; + onCursor: (at: { line: number; column: number }) => void; + /** Everything the language services currently say, across every file. */ + onProblems: (problems: EditorProblem[]) => void; + onSave: () => void; + onQuickOpen: () => void; +} + +const CHECK_OWNER = "bitween-check"; + +const SEVERITY: Record = { + error: monaco.MarkerSeverity.Error, + warning: monaco.MarkerSeverity.Warning, + info: monaco.MarkerSeverity.Info, +}; + +/** + * One Monaco editor over every file of a draft. Each file is its own model, kept for as long as + * the file exists, so switching tabs keeps each file's undo history, cursor and scroll position - + * as in VS Code - and the TypeScript service sees every file at once, so imports between them + * resolve. Loaded on demand (see AdapterEditorPage): Monaco is the heaviest thing in the app. + */ +export default function CodeEditor(props: CodeEditorProps) { + const host = useRef(null); + const editor = useRef(null); + const models = useRef(new Map()); + const viewStates = useRef(new Map()); + const shown = useRef(null); + // The callbacks Monaco holds on to read the latest props, not the first render's. + const latest = useRef(props); + latest.current = props; + + const uriOf = (path: string) => monaco.Uri.parse(`file:///${props.scope}/${path}`); + const pathOf = (uri: monaco.Uri) => { + const prefix = `/${props.scope}/`; + return uri.path.startsWith(prefix) ? uri.path.slice(prefix.length) : null; + }; + + // The editor, once. + useEffect(() => { + const ed = monaco.editor.create(host.current!, { + model: null, + theme: latest.current.theme === "dark" ? "bitween-dark" : "bitween-light", + automaticLayout: true, + fontFamily: '"JetBrains Mono Variable", ui-monospace, "SF Mono", monospace', + fontSize: 13, + lineHeight: 20, + fontLigatures: true, + minimap: { enabled: true, renderCharacters: false, scale: 1 }, + bracketPairColorization: { enabled: true }, + guides: { bracketPairs: "active", indentation: true }, + stickyScroll: { enabled: true }, + smoothScrolling: true, + cursorBlinking: "smooth", + cursorSmoothCaretAnimation: "on", + renderWhitespace: "selection", + scrollBeyondLastLine: false, + padding: { top: 8 }, + fixedOverflowWidgets: true, + formatOnPaste: true, + linkedEditing: true, + "semanticHighlighting.enabled": true, + }); + editor.current = ed; + + ed.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => latest.current.onSave()); + ed.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyP, () => latest.current.onQuickOpen()); + const cursor = ed.onDidChangeCursorPosition((e) => + latest.current.onCursor({ line: e.position.lineNumber, column: e.position.column }), + ); + + const report = () => { + const problems: EditorProblem[] = []; + for (const marker of monaco.editor.getModelMarkers({})) { + const path = pathOf(marker.resource); + if (path === null || marker.owner === CHECK_OWNER) continue; + problems.push({ + path, + line: marker.startLineNumber, + column: marker.startColumn, + message: marker.message, + severity: + marker.severity === monaco.MarkerSeverity.Error + ? "error" + : marker.severity === monaco.MarkerSeverity.Warning + ? "warning" + : "info", + source: marker.source ?? marker.owner, + }); + } + latest.current.onProblems(problems); + }; + const markers = monaco.editor.onDidChangeMarkers(report); + + // Monaco measures its font when it starts; if the web font arrived later, measure again, or the + // cursor sits a little off the characters it is between. + void document.fonts?.ready.then(() => monaco.editor.remeasureFonts()); + + const owned = models.current; + return () => { + cursor.dispose(); + markers.dispose(); + ed.dispose(); + owned.forEach((m) => m.dispose()); + owned.clear(); + editor.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // A model per file: new files get one, removed files lose theirs, and text changed from outside + // (someone else's save, loaded over this one) replaces what the model holds. + useEffect(() => { + for (const [path, text] of Object.entries(props.files)) { + const model = models.current.get(path); + if (!model) { + const created = monaco.editor.createModel(text, languageOf(path), uriOf(path)); + created.onDidChangeContent(() => latest.current.onChange(path, created.getValue())); + models.current.set(path, created); + } else if (model.getValue() !== text) { + model.pushEditOperations([], [{ range: model.getFullModelRange(), text }], () => null); + } + } + for (const [path, model] of models.current) { + if (path in props.files) continue; + model.dispose(); + models.current.delete(path); + viewStates.current.delete(path); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.files]); + + // The file shown, with where you were in it. + useEffect(() => { + const ed = editor.current; + if (!ed) return; + if (shown.current && models.current.has(shown.current)) viewStates.current.set(shown.current, ed.saveViewState()); + const model = props.active ? (models.current.get(props.active) ?? null) : null; + if (ed.getModel() !== model) { + ed.setModel(model); + const state = props.active ? viewStates.current.get(props.active) : null; + if (state) ed.restoreViewState(state); + } + shown.current = props.active; + if (model) ed.focus(); + }, [props.active, props.files]); + + useEffect(() => { + monaco.editor.setTheme(props.theme === "dark" ? "bitween-dark" : "bitween-light"); + }, [props.theme]); + + // The Check's findings, on the lines they name. + useEffect(() => { + for (const [path, model] of models.current) { + monaco.editor.setModelMarkers( + model, + CHECK_OWNER, + props.checkProblems + .filter((p) => p.path === path) + .map((p) => ({ + startLineNumber: p.line, + startColumn: p.column, + endLineNumber: p.line, + endColumn: model.getLineMaxColumn(Math.min(p.line, model.getLineCount())), + message: p.message, + severity: SEVERITY[p.severity], + source: "Check", + })), + ); + } + }, [props.checkProblems, props.files]); + + useEffect(() => { + const ed = editor.current; + const at = props.revealAt; + if (!ed || !at || at.path !== props.active) return; + ed.revealLineInCenter(at.line); + ed.setPosition({ lineNumber: at.line, column: at.column }); + ed.focus(); + }, [props.revealAt, props.active]); + + return
; +} diff --git a/SW.Bitween.Web/ClientApp/src/components/code/__tests__/problems.test.ts b/SW.Bitween.Web/ClientApp/src/components/code/__tests__/problems.test.ts new file mode 100644 index 00000000..6051575b --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/code/__tests__/problems.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { locate } from "../problems"; + +const paths = ["main.py", "lib/helpers.py", "src/index.ts", "adapter.json"]; + +describe("locate", () => { + it("reads the place from each compiler's way of writing it", () => { + expect(locate('File "/build/adapter/lib/helpers.py", line 12, in handle', paths)).toEqual({ + path: "lib/helpers.py", + line: 12, + column: 1, + }); + expect(locate("src/index.ts(7,15): error TS2322: Type 'string'…", paths)).toEqual({ + path: "src/index.ts", + line: 7, + column: 15, + }); + expect(locate("main.py:3:1: E999 SyntaxError: invalid syntax", paths)).toEqual({ + path: "main.py", + line: 3, + column: 1, + }); + }); + + it("only names a file that is in the draft", () => { + expect(locate('File "/usr/lib/python3.12/json/decoder.py", line 355', paths)).toBeNull(); + expect(locate("--describe exited with 1: SyntaxError: invalid syntax", paths)).toBeNull(); + }); +}); diff --git a/SW.Bitween.Web/ClientApp/src/components/code/languages.ts b/SW.Bitween.Web/ClientApp/src/components/code/languages.ts new file mode 100644 index 00000000..276e9997 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/code/languages.ts @@ -0,0 +1,56 @@ +/** Monaco's language for a file, by name. Anything unknown opens as plain text. */ +export function languageOf(path: string): string { + const name = path.toLowerCase(); + const base = name.slice(name.lastIndexOf("/") + 1); + if (base === "dockerfile") return "dockerfile"; + if (base === "requirements.txt") return "plaintext"; + const ext = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : ""; + const byExt: Record = { + py: "python", + js: "javascript", + mjs: "javascript", + cjs: "javascript", + jsx: "javascript", + ts: "typescript", + mts: "typescript", + cts: "typescript", + tsx: "typescript", + json: "json", + md: "markdown", + yml: "yaml", + yaml: "yaml", + xml: "xml", + csproj: "xml", + cs: "csharp", + sh: "shell", + toml: "ini", + ini: "ini", + cfg: "ini", + env: "ini", + sql: "sql", + html: "html", + css: "css", + }; + return byExt[ext] ?? "plaintext"; +} + +const LABELS: Record = { + python: "Python", + javascript: "JavaScript", + typescript: "TypeScript", + json: "JSON", + markdown: "Markdown", + yaml: "YAML", + xml: "XML", + csharp: "C#", + shell: "Shell", + ini: "TOML/INI", + sql: "SQL", + html: "HTML", + css: "CSS", + dockerfile: "Dockerfile", + plaintext: "Plain text", +}; + +/** What the status bar calls a file's language. */ +export const languageLabel = (path: string) => LABELS[languageOf(path)] ?? languageOf(path); diff --git a/SW.Bitween.Web/ClientApp/src/components/code/monacoSetup.ts b/SW.Bitween.Web/ClientApp/src/components/code/monacoSetup.ts new file mode 100644 index 00000000..1e6eca78 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/code/monacoSetup.ts @@ -0,0 +1,88 @@ +import * as monaco from "monaco-editor"; +import EditorWorker from "monaco-editor/editor/editor.worker?worker"; +import TsWorker from "monaco-editor/language/typescript/ts.worker?worker"; +import JsonWorker from "monaco-editor/language/json/json.worker?worker"; +import { languageOf } from "./languages"; + +/* + * Monaco - the editor VS Code is built on - bundled with the app rather than fetched from a CDN: + * the page's Content-Security-Policy allows scripts and workers from this origin only, and an + * instance on a closed network has no CDN to reach. This module is only loaded by the adapter + * editor, so nothing here weighs on any other page. Each language's grammar is its own chunk, + * fetched the first time a file in it is opened, and the TypeScript and JSON workers likewise. + */ + +self.MonacoEnvironment = { + getWorker(_id: string, label: string) { + if (label === "typescript" || label === "javascript") return new TsWorker(); + if (label === "json") return new JsonWorker(); + return new EditorWorker(); + }, +}; + +// The adapter's imports - the SDK, its dependencies - aren't in the browser, so the language +// service can't resolve them. Saying so on every import line would bury the real problems; the +// Check runs the actual build and reports what really fails. +const IGNORED_DIAGNOSTICS = [ + 2307, // Cannot find module + 2792, // Cannot find module, did you mean to set moduleResolution + 7016, // Could not find a declaration file for module + 2875, // JSX tag requires the module path to exist +]; +for (const defaults of [monaco.typescript.typescriptDefaults, monaco.typescript.javascriptDefaults]) { + defaults.setCompilerOptions({ + target: monaco.typescript.ScriptTarget.ESNext, + module: monaco.typescript.ModuleKind.ESNext, + moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs, + allowJs: true, + allowNonTsExtensions: true, + esModuleInterop: true, + strict: true, + }); + defaults.setDiagnosticsOptions({ diagnosticCodesToIgnore: IGNORED_DIAGNOSTICS }); + // Every open file is known to the language service, so an import of another file in the draft + // resolves, completes and type-checks as it would in VS Code. + defaults.setEagerModelSync(true); +} + +// tsconfig and jsconfig are JSON with comments, as TypeScript reads them; everywhere else (package.json +// above all, which npm refuses with a comment in it) a comment is still an error. Nothing is fetched +// to validate against: schemas from the internet aren't reachable from every instance. +monaco.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + enableSchemaRequest: false, + schemas: [ + { + uri: "bitween://schemas/tsconfig.json", + fileMatch: ["**/tsconfig*.json", "**/jsconfig*.json", "tsconfig*.json", "jsconfig*.json"], + schema: { type: "object", allowComments: true, allowTrailingCommas: true }, + }, + ], +}); + +/** VS Code's own dark and light colours, with Bitween's accent where VS Code puts its blue. */ +monaco.editor.defineTheme("bitween-dark", { + base: "vs-dark", + inherit: true, + rules: [], + colors: { + "editor.background": "#1e1e1e", + "editorCursor.foreground": "#f6503d", + "editor.lineHighlightBackground": "#2a2d2e", + "editorLineNumber.activeForeground": "#e3e3e3", + focusBorder: "#f6503d80", + }, +}); +monaco.editor.defineTheme("bitween-light", { + base: "vs", + inherit: true, + rules: [], + colors: { + "editor.background": "#ffffff", + "editorCursor.foreground": "#e3311d", + "editor.lineHighlightBackground": "#f6f3f1", + focusBorder: "#e3311d80", + }, +}); + +export { languageOf, monaco }; diff --git a/SW.Bitween.Web/ClientApp/src/components/code/problems.ts b/SW.Bitween.Web/ClientApp/src/components/code/problems.ts new file mode 100644 index 00000000..61c444d1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/code/problems.ts @@ -0,0 +1,57 @@ +/** One thing wrong (or worth a look) in a draft, at a place in it when one is known. */ +export interface EditorProblem { + path: string; + line: number; + column: number; + message: string; + severity: "error" | "warning" | "info"; + /** Who said so: "Check", "ts", "json". */ + source: string; +} + +/** A message from a Check, with the place it names when it names one of the draft's files. */ +export interface CheckFinding { + message: string; + severity: "error" | "warning"; + at: { path: string; line: number; column: number } | null; +} + +/** + * Finds the file and line in a build message, in the forms the compilers write them: + * + * main.py:12:5: E999 SyntaxError … (python, ruff, tsc --pretty false, gcc-likes) + * src/index.ts(12,5): error TS2322: … (tsc, MSBuild) + * File "/build/adapter/main.py", line 12 (a Python traceback) + * + * A path only counts if it is one of the draft's files - matched by its end, since the build ran + * in a directory of its own and reports absolute or prefixed paths. + */ +export function locate(message: string, paths: string[]): CheckFinding["at"] { + const patterns: RegExp[] = [ + /File "([^"]+)", line (\d+)/, + /([\w./\\-]+\.\w+)\((\d+),(\d+)\)/, + /([\w./\\-]+\.\w+):(\d+)(?::(\d+))?/, + ]; + for (const pattern of patterns) { + const m = message.match(pattern); + if (!m) continue; + const reported = m[1].replace(/\\/g, "/"); + const path = paths + .filter((p) => reported === p || reported.endsWith(`/${p}`)) + .sort((a, b) => b.length - a.length)[0]; + if (!path) continue; + return { path, line: Number(m[2]) || 1, column: Number(m[3]) || 1 }; + } + return null; +} + +export function checkFindings( + build: { problems: string[]; warnings: string[] } | null, + paths: string[], +): CheckFinding[] { + if (!build) return []; + return [ + ...build.problems.map((message) => ({ message, severity: "error" as const, at: locate(message, paths) })), + ...build.warnings.map((message) => ({ message, severity: "warning" as const, at: locate(message, paths) })), + ]; +} diff --git a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx index 31a6b807..8dea78cc 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx @@ -5,27 +5,16 @@ import { ArrowUpRight, Braces, ChevronDown, ChevronRight, Search } from "lucide- import { api, SECRET_SENTINEL, type AdapterInfo, type AdapterKind, type PartnerRow } from "../../api"; import { Button } from "../ui/basics"; import { Field } from "../ui/forms"; -import { AdapterPicker, useAdapterCatalog } from "./AdapterPicker"; +import { AdapterPicker } from "./AdapterPicker"; +import { useAdapterCatalog } from "./adapterCatalog"; import { keys } from "../../api/queryKeys"; -import { NATIVE_MAPPER_ID } from "../../lib/nativeMapper/types"; +import { runtimeLabel } from "../../lib/adapterInventory"; import { lackingPath, lackingPathWarning, type SourceDocument } from "./sourceValues"; +import { usesVisualMappingEditor } from "./mapperEditors"; // Lives with the picker, re-exported here because this is where every screen // already imports it from. -export { useAdapterCatalog }; -/** - * Mappers whose mapping is built in the visual editor rather than typed into - * adapter properties. Both are listed while the old mapper is still in use by - * running subscriptions; its entry goes when they have been moved over. - * - * The old id is spelled out because its module sits outside the app's tsconfig. - */ -const VISUAL_EDITOR_MAPPERS: readonly string[] = ["NativeJSONMapper", NATIVE_MAPPER_ID]; - -export function usesVisualMappingEditor(adapterId: string | null | undefined): boolean { - return adapterId != null && VISUAL_EDITOR_MAPPERS.includes(adapterId); -} /** What the picker is choosing, named for the band above the fields. */ const KIND_LABELS: Record = { @@ -51,7 +40,7 @@ function useReferenceTokens() { label: `${s.id}.${key}`, token: `{{globals.${s.id}.${key}}}`, // A locked value arrives as the sentinel. It is still perfectly usable as a - // reference — the substitution happens on the server — so the token stays in + // reference - the substitution happens on the server - so the token stays in // the list and only its preview is withheld. value: value === SECRET_SENTINEL ? undefined : value, })), @@ -82,13 +71,13 @@ function PartnerPropValue({ partnerId, propKey }: { partnerId: number; propKey: const value = props.data?.[propKey]; if (value === SECRET_SENTINEL) return ( - + secret ); return ( - {value === undefined || value === "" ? "—" : value} + {value === undefined || value === "" ? "-" : value} ); } @@ -151,30 +140,30 @@ function ReferenceMenu({ onClick={() => setOpen((o) => !o)} title="Insert a reference" aria-label={label} - className="mt-1 rounded-md p-1.5 text-ink-400 hover:bg-ink-100 hover:text-ink-700" + className="mt-1 rounded-md p-1.5 text-ink-500 hover:bg-ink-100 hover:text-ink-700" > {open && (
- + setQuery(e.target.value)} placeholder="Search references" aria-label="Search references" - className="h-8 w-full rounded-md border border-ink-200 bg-white pr-2 pl-8 text-[13px] placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" + className="h-8 w-full rounded-md border border-ink-200 bg-white pr-2 pl-8 text-[13px] placeholder:text-ink-400 focus:border-focus-400 focus:ring-2 focus:ring-focus-100 focus:outline-none" />
- {noMatches &&

No matches.

} + {noMatches &&

No matches.

} {/* First: only a subscription fed by a delivery is offered these, and there they are what the field is most likely reaching for. Last, they sat below the scroll. */} {filteredSourceKeys.length > 0 && (
-

+

Original document

@@ -189,7 +178,7 @@ function ReferenceMenu({ className="flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-50" > {t.label} - + e.g. {t.value} @@ -198,7 +187,7 @@ function ReferenceMenu({ )} {filteredGlobals.length > 0 && (

-

+

Global values

{filteredGlobals.map((t) => ( @@ -210,8 +199,8 @@ function ReferenceMenu({ > {t.label} {t.value ?? "secret"} @@ -221,7 +210,7 @@ function ReferenceMenu({ )} {filteredPartnerKeys.length > 0 && (
-

+

Partner properties

{filteredPartnerKeys.map((t) => ( @@ -232,7 +221,7 @@ function ReferenceMenu({ className="flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-50" > {t.label} - resolved per partner + resolved per partner ))}
@@ -259,13 +248,13 @@ function ReferenceHints({ value: string; globals: ReferenceToken[]; partners: PartnerRow[]; - /** Null unless this is a response or bus gateway subscription — nothing else fills `{{source.…}}`. */ + /** Null unless this is a response or bus gateway subscription - nothing else fills `{{source.…}}`. */ source: SourceDocument | null; }) { const [openKeys, setOpenKeys] = useState>(new Set()); - // Set ids and keys are free-form (spaces included) on the backend — match on the + // Set ids and keys are free-form (spaces included) on the backend - match on the // `{{…}}` delimiters themselves, not a restrictive charset, or refs with a space // in the id/key (e.g. "schedule source url") silently fail to match here. const globalRefs = [...new Set([...value.matchAll(/\{\{globals\.[^}]+\}\}/g)].map((m) => m[0]))]; @@ -303,7 +292,7 @@ function ReferenceHints({ {ref.value} ) : ( - not found — check the set and key + not found - check the set and key )}
); @@ -345,8 +334,8 @@ function ReferenceHints({ ))} {without > 0 && ( -
  • - {without} partner{without === 1 ? " doesn't" : "s don't"} set it — their exchanges resolve it +
  • + {without} partner{without === 1 ? " doesn't" : "s don't"} set it - their exchanges resolve it empty.
  • )} @@ -369,7 +358,7 @@ function ReferenceHints({ className="font-medium text-warn-700" title="Saved anyway: documents vary, and the next one may have it. An exchange whose original document has no value there fails, naming the path." > - ⚠ {lackingPathWarning(lacking)} — an exchange without it fails + ⚠ {lackingPathWarning(lacking)} - an exchange without it fails ) : example !== undefined ? ( {masked ? (
    - •••••••• + •••••••• {!disabled && (