Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 980342d566 | |||
| 55bc54c0b8 | |||
| e6397c301d | |||
| 74d40e40a9 | |||
| c81692ee5a | |||
| 7f50987201 | |||
| 7042cdb06a | |||
| af0fc9f669 | |||
| edbc884d74 | |||
| 025d7f70ba | |||
| 70acc06f37 | |||
| a2c99a537a | |||
| 906ff9c3ff | |||
| 515d45528d | |||
| abf86eda03 | |||
| f777050c10 | |||
| c4e956cdb4 | |||
| a662fb54c2 | |||
| 9a74195b24 | |||
| 0ca9162e7b | |||
| 070f21b075 | |||
| 9bf6995b15 | |||
| 43ebc84881 | |||
| 7652cd385d | |||
| a81aaca8fe | |||
| 57843cc454 | |||
| bf6ae0f6af | |||
| 309cead189 | |||
| 4980796cd6 | |||
| 12df19e431 | |||
| c186d98ea7 | |||
| 00a6619cbe |
@@ -0,0 +1,61 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
speckle-sharp-sdk-ref:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
integration-test:
|
||||
env:
|
||||
CLIENT_DIR: "./client"
|
||||
CLIENT_REPO: "specklesystems/speckle-sharp-sdk"
|
||||
SERVER_DIR: "./server"
|
||||
SERVER_REPO: "specklesystems/speckle-server-internal"
|
||||
SOLUTION: "Speckle.Sdk.sln"
|
||||
SPECKLE_SERVER_IMAGE: "speckle-server:local"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout ${{ env.CLIENT_REPO }}
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: ${{ env.CLIENT_DIR }}
|
||||
repository: ${{ env.CLIENT_REPO }}
|
||||
ref: ${{ inputs.speckle-sharp-sdk-ref }}
|
||||
|
||||
- name: Checkout ${{ env.SERVER_REPO }}
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ env.SERVER_REPO }}
|
||||
path: ${{ env.SERVER_DIR }}
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
# cache: true
|
||||
# cache-dependency-path: "**/packages.lock.json"
|
||||
|
||||
- name: 🏗️ Build Server
|
||||
run: docker build --file "./packages/server/Dockerfile" --tag ${{ env.SPECKLE_SERVER_IMAGE }} .
|
||||
working-directory: ${{ env.SERVER_DIR }}
|
||||
|
||||
- name: ⚙️ Spin up Server
|
||||
run: docker compose --file "../${{ env.CLIENT_DIR }}/docker-compose-internal.yml" up --wait
|
||||
working-directory: ${{ env.SERVER_DIR }}
|
||||
env:
|
||||
SPECKLE_SERVER_IMAGE: ${{ env.SPECKLE_SERVER_IMAGE }}
|
||||
|
||||
- name: 📦 Restore .NET Solution
|
||||
run: dotnet restore ${{ env.SOLUTION }} --locked-mode
|
||||
working-directory: ${{ env.CLIENT_DIR }}
|
||||
|
||||
- name: 🏗️ Build .NET Solution
|
||||
run: dotnet build ${{ env.SOLUTION }} --configuration Release --no-restore -warnaserror
|
||||
working-directory: ${{ env.CLIENT_DIR }}
|
||||
|
||||
- name: 🔨 Run .NET Integration Tests
|
||||
run: dotnet test ${{ env.SOLUTION }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal
|
||||
working-directory: ${{ env.CLIENT_DIR }}
|
||||
@@ -30,14 +30,14 @@ jobs:
|
||||
|
||||
- name: 🔐 Login to Github Container Registry
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: ⚙️ Spin up Server
|
||||
run: docker compose -f ${{ inputs.docker-compose-file }} up --wait
|
||||
run: docker compose --file ${{ inputs.docker-compose-file }} up --wait
|
||||
|
||||
- name: 📦 Restore
|
||||
run: dotnet restore ${{ env.Solution }} --locked-mode
|
||||
@@ -51,10 +51,10 @@ jobs:
|
||||
|
||||
- name: 🔨 Integration Tests against Internal Server
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
name: PR Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- "main" # Need to run for codecov to compare against the BASE
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -38,7 +42,7 @@ jobs:
|
||||
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -46,13 +46,6 @@ jobs:
|
||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: NuGet login (OIDC → temp API key)
|
||||
uses: NuGet/login@v1
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[9.0.4,)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[5.0.0,)" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
||||
<PackageVersion Include="Open.ChannelExtensions" Version="9.1.0" />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Build.Targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="docker-compose-internal.yml" />
|
||||
<File Path="docker-compose.yml" />
|
||||
<File Path="global.json" />
|
||||
<File Path="README.md" />
|
||||
@@ -17,6 +18,7 @@
|
||||
<File Path=".github\git-commit-instructions.md" />
|
||||
</Folder>
|
||||
<Folder Name="/config/workflows/">
|
||||
<File Path=".github/workflows/integration-test-callable-from-server-repo.yml" />
|
||||
<File Path=".github/workflows/integration-test.yml" />
|
||||
<File Path=".github/workflows/pr.yml" />
|
||||
<File Path=".github/workflows/release.yml" />
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ Target(
|
||||
|
||||
Target(
|
||||
PACK,
|
||||
dependsOn: [TEST],
|
||||
dependsOn: [BUILD],
|
||||
async () =>
|
||||
{
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
start_period: 10s
|
||||
|
||||
speckle-server:
|
||||
image: ghcr.io/specklesystems/speckle-server:latest
|
||||
image: ${SPECKLE_SERVER_IMAGE:-ghcr.io/specklesystems/speckle-server:latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Nugetspec Package Properties">
|
||||
<PackageId>Speckle.Automate.Sdk</PackageId>
|
||||
|
||||
@@ -1,366 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json.Schema": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.0.1, )",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"System.CommandLine": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.0-beta4.22272.1, )",
|
||||
"resolved": "2.0.0-beta4.22272.1",
|
||||
"contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.5, )",
|
||||
"resolved": "8.0.5",
|
||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encodings.Web": "8.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions.Websocket": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "FTerRmQPqHrCrnoUzhBu+E+1DNGwyrAMLqHkAqOOOu5pGfyMOj8qQUBxI/gDtWtG11p49UxSfWmBzRNlwZqfUg==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.ComponentModel.Annotations": "4.5.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
|
||||
"dependencies": {
|
||||
"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encodings.Web": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"speckle.objects": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Speckle.Sdk": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"GraphQL.Client": "[6.0.0, )",
|
||||
"Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )",
|
||||
"Microsoft.CSharp": "[4.7.0, )",
|
||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||
"Microsoft.Extensions.Logging": "[2.2.0, )",
|
||||
"Speckle.DoubleNumerics": "[4.1.0, )",
|
||||
"Speckle.Newtonsoft.Json": "[13.0.2, )",
|
||||
"Speckle.Sdk.Dependencies": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk.dependencies": {
|
||||
"type": "Project"
|
||||
},
|
||||
"GraphQL.Client": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0",
|
||||
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.7.0, )",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[7.0.5, )",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "KGxbPeWsQMnmQy43DSBxAFtHz3l2JX8EWBSGUCvT3CuZ8KsuzbkqMIJMDOxWtG8eZSoCDI04aiVQjWuuV8HmSw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "7.0.5",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "2.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Speckle.DoubleNumerics": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "20DtS+FsDRsOD9+AU3TwNFZ0qrKo5f6f7B5ZR9wStsIHHHC9k7DpjbCvuNtmnSjx54MD+TJC7wV2f5iyGVPj1A=="
|
||||
},
|
||||
"Speckle.Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[13.0.2, )",
|
||||
"resolved": "13.0.2",
|
||||
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<PolySharpExcludeGeneratedTypes>System.Runtime.CompilerServices.RequiresLocationAttribute</PolySharpExcludeGeneratedTypes>
|
||||
<Configurations>Debug;Release;Local</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,313 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"GraphQL.Client.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions.Websocket": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "FTerRmQPqHrCrnoUzhBu+E+1DNGwyrAMLqHkAqOOOu5pGfyMOj8qQUBxI/gDtWtG11p49UxSfWmBzRNlwZqfUg==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.ComponentModel.Annotations": "4.5.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw=="
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.4.0",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.2"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
|
||||
"dependencies": {
|
||||
"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
"contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw=="
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"speckle.sdk": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"GraphQL.Client": "[6.0.0, )",
|
||||
"Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )",
|
||||
"Microsoft.CSharp": "[4.7.0, )",
|
||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||
"Microsoft.Extensions.Logging": "[2.2.0, )",
|
||||
"Speckle.DoubleNumerics": "[4.1.0, )",
|
||||
"Speckle.Newtonsoft.Json": "[13.0.2, )",
|
||||
"Speckle.Sdk.Dependencies": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk.dependencies": {
|
||||
"type": "Project"
|
||||
},
|
||||
"GraphQL.Client": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0",
|
||||
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.7.0, )",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[7.0.5, )",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "KGxbPeWsQMnmQy43DSBxAFtHz3l2JX8EWBSGUCvT3CuZ8KsuzbkqMIJMDOxWtG8eZSoCDI04aiVQjWuuV8HmSw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "7.0.5",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "2.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Speckle.DoubleNumerics": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "20DtS+FsDRsOD9+AU3TwNFZ0qrKo5f6f7B5ZR9wStsIHHHC9k7DpjbCvuNtmnSjx54MD+TJC7wV2f5iyGVPj1A=="
|
||||
},
|
||||
"Speckle.Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[13.0.2, )",
|
||||
"resolved": "13.0.2",
|
||||
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
namespace Speckle.Sdk.Dependencies;
|
||||
|
||||
public sealed class ArrayBufferWriterPooledObjectPolicy<T> : PooledObjectPolicy<ArrayBufferWriter<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the initial capacity of pooled <see cref="ArrayBufferWriter{T}"/> instances.
|
||||
/// </summary>
|
||||
/// <value>Defaults to <c>100</c>.</value>
|
||||
public int InitialCapacity { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum value for <see cref="StringBuilder.Capacity"/> that is allowed to be
|
||||
/// retained, when <see cref="Return(ArrayBufferWriter{T})"/> is invoked.
|
||||
/// </summary>
|
||||
/// <value>Defaults to <c>4096</c>.</value>
|
||||
public int MaximumRetainedCapacity { get; set; } = 4 * 1024;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ArrayBufferWriter<T> Create()
|
||||
{
|
||||
return new ArrayBufferWriter<T>(InitialCapacity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Return(ArrayBufferWriter<T> obj)
|
||||
{
|
||||
if (obj.Capacity > MaximumRetainedCapacity)
|
||||
{
|
||||
// Too big. Discard this one.
|
||||
return false;
|
||||
}
|
||||
|
||||
obj.Clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public interface ISdkActivity : IDisposable
|
||||
void SetTag(string key, object? value);
|
||||
void RecordException(Exception e);
|
||||
string TraceId { get; }
|
||||
string SpanId { get; }
|
||||
void SetStatus(SdkActivityStatusCode code);
|
||||
|
||||
void InjectHeaders(Action<string, string> header);
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Speckle.Connectors.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Logging;
|
||||
|
||||
public interface ISdkActivityFactory : IDisposable
|
||||
{
|
||||
ISdkActivity? Start(string? name = default, [CallerMemberName] string source = "");
|
||||
ISdkActivity? Start(
|
||||
string? name = null,
|
||||
SdkActivityKind kind = SdkActivityKind.Internal,
|
||||
[CallerMemberName] string source = ""
|
||||
);
|
||||
|
||||
ISdkActivity? StartRemote(
|
||||
string traceContext,
|
||||
SdkActivityKind kind,
|
||||
string? name = null,
|
||||
[CallerMemberName] string source = ""
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
@@ -24,6 +25,9 @@ public static class Pools
|
||||
public static Pool<StringBuilder> StringBuilders { get; } =
|
||||
new(new StringBuilderPooledObjectPolicy() { MaximumRetainedCapacity = 100 * 1024 * 1024 });
|
||||
|
||||
public static Pool<ArrayBufferWriter<byte>> ArrayBufferWriter { get; } =
|
||||
new(new ArrayBufferWriterPooledObjectPolicy<byte>() { MaximumRetainedCapacity = 100 * 1024 * 1024 });
|
||||
|
||||
private sealed class ObjectDictionaryPolicy<TKey, TValue> : IPooledObjectPolicy<Dictionary<TKey, TValue>>
|
||||
where TKey : notnull
|
||||
{
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Speckle.Sdk.Dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// For various reasons related to our use of ILRepack.FullAuto,
|
||||
/// we cannot use Channels from the SDK project.
|
||||
/// We have to keep usage of it inside the Sdk.Dependencies project.
|
||||
///
|
||||
/// For the sake of quick development, I've wrapped the <see cref="Channel"/> class here in a type
|
||||
/// that is safe to use from the SDK project.
|
||||
///
|
||||
/// As and when we need more functions, we can add them here.
|
||||
///
|
||||
/// And yes... I'm not very happy about the way we've set this up
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public sealed class RepackedChannel<T>
|
||||
{
|
||||
private readonly Channel<T> _channel;
|
||||
|
||||
public RepackedChannel(int capacity, bool singleReader, bool singleWriter)
|
||||
{
|
||||
_channel = Channel.CreateBounded<T>(
|
||||
new BoundedChannelOptions(capacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = singleReader,
|
||||
SingleWriter = singleWriter,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void CompleteWriter() => _channel.Writer.Complete();
|
||||
|
||||
public ValueTask WriteAsync(T item, CancellationToken cancellationToken) =>
|
||||
_channel.Writer.WriteAsync(item, cancellationToken);
|
||||
|
||||
public IAsyncEnumerable<T> ReadAllAsync(CancellationToken cancellationToken) =>
|
||||
_channel.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
// public async Task ReadAllAsync(Func<T, Task> callback, CancellationToken cancellationToken)
|
||||
// {
|
||||
// await foreach (T item in _channel.Reader.ReadAllAsync(cancellationToken))
|
||||
// {
|
||||
// await callback.Invoke(item).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Configurations>Debug;Release;Local</Configurations>
|
||||
<ILRepackTargetConfigurations>Debug;Release;Local</ILRepackTargetConfigurations>
|
||||
<ILRepackRenameInternalized>true</ILRepackRenameInternalized>
|
||||
@@ -28,4 +28,37 @@
|
||||
<PackageReference Include="Open.ChannelExtensions" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Threading.Channels" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
<Target Name="BeforeILRepackPrepareBuild" BeforeTargets="ILRepackPrepareBuild">
|
||||
<ItemGroup>
|
||||
<!--
|
||||
We're Being selective about which assemblies we're il-repacking
|
||||
Avoiding repacling `Microsoft.Bcl.AsyncInterfaces.dll` because we need types like `ValueTask` and `IAsyncEnumerable` to be external
|
||||
|
||||
Yes, this does beg the question, why are we using `IlRepack.FullAuto` instead of raw ILRepack. Well the truth is, I'd like to move away from FullAuto
|
||||
since it's unmaintaned and is lagging behind ILRepack version.
|
||||
-->
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)System.Numerics.Vectors.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)System.Runtime.CompilerServices.Unsafe.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)System.Memory.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)Open.ChannelExtensions.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)System.Threading.Channels.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)System.Collections.Immutable.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)Polly.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)Polly.Contrib.WaitAndRetry.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)Polly.Extensions.Http.dll" />
|
||||
<_ILRepackIncludeAssemblies_Items Include="$(OutputPath)Microsoft.Extensions.ObjectPool.dll" />
|
||||
<_ILRepackExcludeAssemblies_Items Include="$(OutputPath)*.dll" Exclude="@(_ILRepackIncludeAssemblies_Items)" />
|
||||
</ItemGroup>
|
||||
<Message
|
||||
Text="These are the packages we are NOT ilrepacking '@(_ILRepackExcludeAssemblies_Items)'"
|
||||
Importance="high"
|
||||
/>
|
||||
<PropertyGroup>
|
||||
<ILRepackExcludeAssemblies>@(_ILRepackExcludeAssemblies_Items)</ILRepackExcludeAssemblies>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Speckle.Connectors.Logging;
|
||||
|
||||
public enum SdkActivityKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Default value.
|
||||
/// Indicates that the Activity represents an internal operation within an application, as opposed to an operations with remote parents or children.
|
||||
/// </summary>
|
||||
Internal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Server activity represents request incoming from external component.
|
||||
/// </summary>
|
||||
Server = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Client activity represents outgoing request to the external component.
|
||||
/// </summary>
|
||||
Client = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Producer activity represents output provided to external components.
|
||||
/// </summary>
|
||||
Producer = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Consumer activity represents output received from an external component.
|
||||
/// </summary>
|
||||
Consumer = 4,
|
||||
}
|
||||
@@ -1,167 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"ILRepack.FullAuto": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.6.0, )",
|
||||
"resolved": "1.6.0",
|
||||
"contentHash": "34qp/HQ0XRIWCjtNGUOslJ6p9eNWqHXZQ+xx1iBCvXy3mj8tEiqIwRG+LubFyKCJITqMh5cpFvFl20/6+Dmy+g==",
|
||||
"dependencies": {
|
||||
"ILRepack": "2.0.33"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "G7p1k2xVZ+2aVANz0JdSiafr+AHDHeS1kF8+Y0ABbIsByd0erOL59IDXBs9vcdJf3pPV/murO0mbtr4k40QxWw=="
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"Open.ChannelExtensions": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.1.0, )",
|
||||
"resolved": "9.1.0",
|
||||
"contentHash": "D6c24vMGy1oZ06vmkD2/FNzWHK7ZIihuv2spDgYEeaUp+eobrILQnrNQKRoASFXD4JGfZ7nfvTM0e+AX79dt8Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
|
||||
"System.Collections.Immutable": "9.0.4",
|
||||
"System.Threading.Channels": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Polly": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.2.3, )",
|
||||
"resolved": "7.2.3",
|
||||
"contentHash": "DeCY0OFbNdNxsjntr1gTXHJ5pKUwYzp04Er2LLeN3g6pWhffsGuKVfMBLe1lw7x76HrPkLxKEFxBlpRxS2nDEQ=="
|
||||
},
|
||||
"Polly.Contrib.WaitAndRetry": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.1.1, )",
|
||||
"resolved": "1.1.1",
|
||||
"contentHash": "1MUQLiSo4KDkQe6nzQRhIU05lm9jlexX5BVsbuw0SL82ynZ+GzAHQxJVDPVBboxV37Po3SG077aX8DuSy8TkaA=="
|
||||
},
|
||||
"Polly.Extensions.Http": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.0, )",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
|
||||
"dependencies": {
|
||||
"Polly": "7.1.0"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"System.Threading.Channels": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "4qBn2H6/aXBpE/Pm3wY5yusY/pEvQz99NlWHrTUji0qCmOdbhhjaALcpmbfW2ksxlPM6i6S+QFLkpOQdyfeKYQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"ILRepack": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.33",
|
||||
"contentHash": "xb2h1CsOepoYwdXEPui9VcQglwABQwNf9cccZbf+acarEzF5PUp8Xx71nFXIhOgEdm6wrxAoF6xAxK4m/XFRUQ=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "wfm2NgK22MmBe5qJjp52qzpkeDZKb4l9LbdubhZSehY1z4LS+lld6R+B+UQNb2AZRHu/QJlHxEUcRst5hIEejg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"ILRepack.FullAuto": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -193,30 +193,7 @@ public sealed class BlobApi : IBlobApi
|
||||
using var response = await _unauthedClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return ParseEtagHeader(response.Headers);
|
||||
}
|
||||
|
||||
private static string ParseEtagHeader(HttpResponseHeaders headers)
|
||||
{
|
||||
if (!headers.TryGetValues("ETag", out var etagValues))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Response does not have an ETag attached to it, cannot use this as an upload",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
var etagValuesArray = etagValues.ToArray();
|
||||
|
||||
if (etagValuesArray.Length != 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected Etag header to have a single value but got {etagValuesArray.Length}",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
return etagValuesArray[0];
|
||||
return BlobApiHelpers.ParseEtagHeader(response.Headers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -87,6 +87,26 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
catch (Exception ex) when (!ex.IsFatal()) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the <see cref="GQLClient"/>'s websocket is fully initialized.
|
||||
/// <br/>
|
||||
/// You don't <i>need</i> to call this function, if you don't, then it will be setup for you when you call <see cref="SubscribeTo"/> (e.g. when you create a <see cref="Subscription"/>),
|
||||
/// but due to <see cref="GraphQL"/>'s WebSocket implementation, it's not awaited (deferred) thus the subscription make take a while to actually be setup.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We only use websockets for GraphQL subscriptions, so if you're not using subscriptions, don't call this
|
||||
///
|
||||
/// Note. due to other sources (potentially on the GraphQL side) you still need a ~100ms delay between setting up the subscription, and being able to relaibly trigger it
|
||||
/// This should only really negatively affect test projects.
|
||||
/// </remarks>
|
||||
public async Task InitializeWebsocket()
|
||||
{
|
||||
if (GQLClient.WebSocketSubProtocol is null)
|
||||
{
|
||||
await GQLClient.InitializeWebsocketConnection().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<T> ExecuteWithResiliencePolicies<T>(Func<Task<T>> func) =>
|
||||
await GraphQLRetry
|
||||
.ExecuteAsync<T, SpeckleGraphQLInternalErrorException>(
|
||||
@@ -134,10 +154,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
// Don't record exception as it's rethrown.
|
||||
activity?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ public record ModelIngestionCreateInput(
|
||||
string modelId,
|
||||
string projectId,
|
||||
string progressMessage,
|
||||
SourceDataInput sourceData
|
||||
SourceDataInput sourceData,
|
||||
int? maxIdleTimeoutSeconds = null
|
||||
);
|
||||
|
||||
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
||||
|
||||
@@ -6,7 +6,8 @@ public sealed class ModelIngestion
|
||||
public required DateTime createdAt { get; init; }
|
||||
public required DateTime updatedAt { get; init; }
|
||||
public required string modelId { get; init; }
|
||||
public required string projectId { get; init; }
|
||||
public required string userId { get; init; }
|
||||
public required bool cancellationRequested { get; init; }
|
||||
public required ModelIngestionStatusData statusData { get; init; }
|
||||
// public required LimitedUser user { get; init; }
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ public sealed class ModelIngestionStatusData
|
||||
{
|
||||
public required ModelIngestionStatus status { get; init; }
|
||||
public required string? progressMessage { get; init; }
|
||||
public required string? versionId { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelPermissionChecks
|
||||
{
|
||||
public PermissionCheckResult canUpdate { get; init; }
|
||||
public PermissionCheckResult canDelete { get; init; }
|
||||
public PermissionCheckResult canCreateVersion { get; init; }
|
||||
}
|
||||
@@ -5,5 +5,7 @@ public sealed class ProjectPermissionChecks
|
||||
public PermissionCheckResult canCreateModel { get; init; }
|
||||
public PermissionCheckResult canDelete { get; init; }
|
||||
public PermissionCheckResult canLoad { get; init; }
|
||||
|
||||
[Obsolete("Use ModelPermissionChecks.CanCreateVersion instead", true)]
|
||||
public PermissionCheckResult canPublish { get; init; }
|
||||
}
|
||||
|
||||
@@ -397,11 +397,6 @@ public sealed class ActiveUserResource
|
||||
authorized
|
||||
message
|
||||
}
|
||||
canPublish {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
public sealed class ModelIngestionResource
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public sealed class ModelIngestionResource
|
||||
/// <remarks>
|
||||
/// The model ingestion created will have a <c>processing</c> state (not <c>queued</c>). This mutation is designed to be used
|
||||
/// by client/connectors that are immediately processing
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
@@ -44,6 +44,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -72,7 +74,7 @@ public sealed class ModelIngestionResource
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="modelIngestionId"></param>
|
||||
/// <param name="projectId"></param>
|
||||
@@ -94,6 +96,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -102,6 +106,10 @@ public sealed class ModelIngestionResource
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
... on ModelIngestionSuccessStatus
|
||||
{
|
||||
versionId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +129,7 @@ public sealed class ModelIngestionResource
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
@@ -142,6 +150,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -173,7 +183,7 @@ public sealed class ModelIngestionResource
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
@@ -194,6 +204,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -222,7 +234,7 @@ public sealed class ModelIngestionResource
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
@@ -245,6 +257,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -277,7 +291,7 @@ public sealed class ModelIngestionResource
|
||||
/// If successful, the job will be in a terminal "successful" state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
@@ -320,7 +334,7 @@ public sealed class ModelIngestionResource
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
@@ -343,6 +357,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -375,7 +391,7 @@ public sealed class ModelIngestionResource
|
||||
/// This should only be done if the user has explicitly requested cancellation
|
||||
/// Other forms of cancellation use <see cref="FailWithError"/>.
|
||||
/// The ingestion should then enter a terminal "canceled" state.<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </summary>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
@@ -398,6 +414,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -434,7 +452,7 @@ public sealed class ModelIngestionResource
|
||||
/// via <see cref="SubscriptionResource.CreateProjectModelIngestionCancellationRequestedSubscription"/>
|
||||
/// and report it as canceled via <see cref="ModelIngestionResource.FailWithCancel"/>
|
||||
/// See "cooperative cancellation pattern"<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
@@ -457,6 +475,8 @@ public sealed class ModelIngestionResource
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
|
||||
@@ -312,4 +312,88 @@ public sealed class ModelResource
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelPermissionChecks> GetPermissions(
|
||||
string projectId,
|
||||
string modelId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:permissions {
|
||||
canUpdate {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canDelete {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canCreateVersion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelPermissionChecks>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="SpeckleGraphQLBadInputException">server versions <3.0.11 do not have <c>canCreateIngestion</c> and will throw this exception</exception>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<PermissionCheckResult> CanCreateModelIngestion(
|
||||
string projectId,
|
||||
string modelId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:permissions {
|
||||
data:canCreateIngestion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<
|
||||
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<PermissionCheckResult>>>>
|
||||
>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,8 @@ public sealed class SubscriptionResource : IDisposable
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
projectId
|
||||
userId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
@@ -237,6 +239,10 @@ public sealed class SubscriptionResource : IDisposable
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
... on ModelIngestionSuccessStatus
|
||||
{
|
||||
versionId
|
||||
}
|
||||
}
|
||||
}
|
||||
type
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
schema: https://app.speckle.systems/graphql
|
||||
schema: https://latest.speckle.systems/graphql
|
||||
documents: '**/*.graphql'
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
[ClassInterface(ClassInterfaceType.AutoDual)]
|
||||
[ComVisible(true)]
|
||||
public class Account : IEquatable<Account>
|
||||
{
|
||||
private string _id;
|
||||
@@ -37,6 +34,8 @@ public class Account : IEquatable<Account>
|
||||
public string? refreshToken { get; set; }
|
||||
|
||||
public bool isDefault { get; set; }
|
||||
|
||||
[Obsolete("Not used in v3")]
|
||||
public bool isOnline { get; set; } = true;
|
||||
|
||||
public ServerInfo serverInfo { get; set; }
|
||||
@@ -101,33 +100,4 @@ public class Account : IEquatable<Account>
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal const string LOCAL_IDENTIFIER_DEPRECATION_MESSAGE = "Local identifiers no longer nesseary";
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local identifier for the current user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="Uri"/> object representing the local identifier for the current user.
|
||||
/// The local identifier is created by appending the user ID as a query parameter to the server URL.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Notice that the generated Uri is not intended to be used as a functioning Uri, but rather as a
|
||||
/// unique identifier for a specific account in a local environment. The format of the Uri, containing a query parameter with the user ID,
|
||||
/// serves this specific purpose. Therefore, it should not be used for forming network requests or
|
||||
/// expecting it to lead to an actual webpage. The primary intent of this Uri is for unique identification in a Uri format.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// This sample shows how to call the GetLocalIdentifier method.
|
||||
/// <code>
|
||||
/// Uri localIdentifier = GetLocalIdentifier();
|
||||
/// Console.WriteLine(localIdentifier);
|
||||
/// </code>
|
||||
/// For a fictional `User ID: 123` and `Server: https://speckle.xyz`, the output might look like this:
|
||||
/// <code>
|
||||
/// https://speckle.xyz?id=123
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Obsolete(LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
internal Uri GetLocalIdentifier() => new($"{serverInfo.url}?id={userInfo.id}");
|
||||
}
|
||||
|
||||
@@ -1,145 +1,35 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.SQLite;
|
||||
using Stream = System.IO.Stream;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
public partial interface IAccountManager : IDisposable;
|
||||
|
||||
/// <summary>
|
||||
/// Manage accounts locally for desktop applications.
|
||||
/// Manages <see cref="Account"/> data in the local sqlite account store
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public sealed class AccountManager(
|
||||
ISpeckleApplication application,
|
||||
ILogger<AccountManager> logger,
|
||||
IGraphQLClientFactory graphQLClientFactory,
|
||||
ISpeckleHttp speckleHttp,
|
||||
IAccountFactory accountFactory,
|
||||
IAuthFlow authFlow,
|
||||
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
|
||||
) : IAccountManager
|
||||
{
|
||||
public const string DEFAULT_SERVER_URL = "https://app.speckle.systems";
|
||||
|
||||
private readonly ISqLiteJsonCacheManager _accountStorage = sqLiteJsonCacheManagerFactory.CreateForUser("Accounts");
|
||||
private static volatile bool s_isAddingAccount;
|
||||
private readonly ISqLiteJsonCacheManager _accountAddLockStorage = sqLiteJsonCacheManagerFactory.CreateForUser(
|
||||
"AccountAddFlow"
|
||||
);
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public void Dispose()
|
||||
{
|
||||
_accountStorage.Dispose();
|
||||
_accountAddLockStorage.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the basic information about a server.
|
||||
/// </summary>
|
||||
/// <param name="server">Server Information</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<ServerInfo> GetServerInfo(Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var gqlClient = graphQLClientFactory.CreateGraphQLClient(server, null);
|
||||
|
||||
//lang=graphql
|
||||
const string QUERY_STRING = "query { serverInfo { name company migration { movedFrom movedTo } } }";
|
||||
|
||||
var request = new GraphQLRequest { Query = QUERY_STRING };
|
||||
|
||||
var response = await gqlClient.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
|
||||
return response.Data.serverInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets basic user information given a token and a server.
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<UserInfo> GetUserInfo(string token, Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var gqlClient = graphQLClientFactory.CreateGraphQLClient(server, token);
|
||||
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query {
|
||||
data:activeUser {
|
||||
name
|
||||
email
|
||||
id
|
||||
company
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await gqlClient
|
||||
.SendQueryAsync<RequiredResponse<UserInfo>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
return response.Data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Default Server URL for authentication, can be overridden by placing a file with the alternatrive url in the Speckle folder or with an ENV_VAR
|
||||
/// </summary>
|
||||
public Uri GetDefaultServerUrl()
|
||||
{
|
||||
var customServerUrl = "";
|
||||
|
||||
// first mechanism, check for local file
|
||||
var customServerFile = Path.Combine(SpecklePathProvider.UserSpeckleFolderPath, "server");
|
||||
if (File.Exists(customServerFile))
|
||||
{
|
||||
customServerUrl = File.ReadAllText(customServerFile);
|
||||
}
|
||||
|
||||
// second mechanism, check ENV VAR
|
||||
var customServerEnvVar = Environment.GetEnvironmentVariable("SPECKLE_SERVER");
|
||||
if (!string.IsNullOrEmpty(customServerEnvVar))
|
||||
{
|
||||
customServerUrl = customServerEnvVar;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(customServerUrl))
|
||||
{
|
||||
if (Uri.TryCreate(customServerUrl, UriKind.Absolute, out Uri? url))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return new Uri(DEFAULT_SERVER_URL);
|
||||
}
|
||||
|
||||
/// <param name="id">The Id of the account to fetch</param>
|
||||
@@ -151,37 +41,6 @@ public sealed class AccountManager(
|
||||
?? throw new SpeckleAccountManagerException($"Account {id} not found");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades an account from the account.serverInfo.movedFrom account to the account.serverInfo.movedTo account
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the account to upgrade</param>
|
||||
public void UpgradeAccount(string id)
|
||||
{
|
||||
Account account = GetAccount(id);
|
||||
|
||||
if (account.serverInfo.migration?.movedTo is not Uri upgradeUri)
|
||||
{
|
||||
throw new SpeckleAccountManagerException(
|
||||
$"Server with url {account.serverInfo.url} does not have information about the upgraded server"
|
||||
);
|
||||
}
|
||||
|
||||
account.serverInfo.migration.movedTo = null;
|
||||
account.serverInfo.migration.movedFrom = new Uri(account.serverInfo.url);
|
||||
account.serverInfo.url = upgradeUri.ToString().TrimEnd('/');
|
||||
|
||||
// setting the id to null will force it to be recreated
|
||||
account.id = null!; //TODO this is gross so remove when id is nullable
|
||||
|
||||
RemoveAccount(id);
|
||||
_accountStorage.UpdateObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
|
||||
}
|
||||
|
||||
public IEnumerable<Account> GetAccounts(string serverUrl)
|
||||
{
|
||||
return GetAccounts(new Uri(serverUrl));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all unique accounts matching the serverUrl provided. If an account exists on more than one server,
|
||||
/// typically because it has been migrated, then only the upgraded account (and therefore server) are returned.
|
||||
@@ -245,7 +104,6 @@ public sealed class AccountManager(
|
||||
static bool IsInvalid(Account ac) => ac.userInfo == null || ac.serverInfo == null;
|
||||
|
||||
var sqlAccounts = _accountStorage.GetAllObjects().Select(x => JsonConvert.DeserializeObject<Account>(x.Json));
|
||||
var localAccounts = GetLocalAccounts();
|
||||
|
||||
foreach (var acc in sqlAccounts)
|
||||
{
|
||||
@@ -259,119 +117,55 @@ public sealed class AccountManager(
|
||||
yield return acc;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var acc in localAccounts)
|
||||
{
|
||||
yield return acc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local accounts
|
||||
/// These are accounts not handled by Manager and are stored in json format in a local directory
|
||||
/// Refetches all local accounts (in local db), including <see cref="ServerInfo"/> and <see cref="UserInfo"/>.
|
||||
/// If the <see cref="Account.token"/> looks to be expired, this function will also attempt to use the <see cref="Account.refreshToken"/> to refresh it.
|
||||
/// Will write the changes to the local accounts db
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IList<Account> GetLocalAccounts()
|
||||
/// <seealso cref="UpdateAccount"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="AggregateException"></exception>
|
||||
public async Task UpdateAccount(Account account, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var accountsDir = SpecklePathProvider.AccountsFolderPath;
|
||||
if (!Directory.Exists(accountsDir))
|
||||
string oldAccountId = account.id;
|
||||
await UpdateAccountInMemory(account, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (oldAccountId != account.id)
|
||||
{
|
||||
return Array.Empty<Account>();
|
||||
// ID may have changed, e.g. users email changed, or server url migrated
|
||||
_accountStorage.DeleteObject(oldAccountId);
|
||||
}
|
||||
|
||||
var accounts = new List<Account>();
|
||||
string[] files = Directory.GetFiles(accountsDir, "*.json", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
Account? account = JsonConvert.DeserializeObject<Account>(json);
|
||||
|
||||
if (
|
||||
account is not null
|
||||
&& !string.IsNullOrEmpty(account.token)
|
||||
&& !string.IsNullOrEmpty(account.userInfo.id)
|
||||
&& !string.IsNullOrEmpty(account.userInfo.email)
|
||||
&& !string.IsNullOrEmpty(account.userInfo.name)
|
||||
&& !string.IsNullOrEmpty(account.serverInfo.url)
|
||||
&& !string.IsNullOrEmpty(account.serverInfo.name)
|
||||
)
|
||||
{
|
||||
accounts.Add(account);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to load json account at {filePath}", file);
|
||||
}
|
||||
}
|
||||
|
||||
return accounts;
|
||||
_accountStorage.UpdateObject(account.id, JsonConvert.SerializeObject(account));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refetches user and server info for each account
|
||||
/// </summary>
|
||||
/// <param name="app"> It is defaultAppId in the server. By default it is "sca" to not break existing parts that this function involves.</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateAccounts(CancellationToken ct = default, string app = "sca")
|
||||
{
|
||||
// need to ToList() the GetAccounts call or the UpdateObject call at the end of this method
|
||||
// will not work because sqlite does not support concurrent db calls
|
||||
foreach (var account in GetAccounts().ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri url = new(account.serverInfo.url);
|
||||
var userServerInfo = await accountFactory.GetUserServerInfo(url, account.token, ct).ConfigureAwait(false);
|
||||
|
||||
//the token has expired
|
||||
//TODO: once we get a token expired exception from the server use that instead
|
||||
if (userServerInfo.activeUser == null || userServerInfo.serverInfo == null)
|
||||
{
|
||||
// We were initially was handling refresh token here bc quite a while ago server was returning null
|
||||
// for activeUser and serverInfo instead of throwing exception. In short, our logic moved into catch block to cover both.
|
||||
throw new SpeckleException("Token is expired");
|
||||
}
|
||||
|
||||
account.isOnline = true;
|
||||
account.userInfo = userServerInfo.activeUser;
|
||||
account.serverInfo = userServerInfo.serverInfo;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
await RefreshAndSetAccountToken(account, app).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_accountStorage.UpdateObject(account.id, JsonConvert.SerializeObject(account));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mutates the account with new tokens.
|
||||
/// Refetches the <paramref name="account"/> information, including <see cref="ServerInfo"/> and <see cref="UserInfo"/>
|
||||
///
|
||||
/// Will only mutate <paramref name="account"/> in memory only, and only if successful.
|
||||
/// </summary>
|
||||
/// <seealso cref="UpdateAccount"/>
|
||||
/// <param name="account"></param>
|
||||
/// <param name="app"></param>
|
||||
private async Task RefreshAndSetAccountToken(Account account, string app)
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="GraphQLHttpRequestException"></exception>
|
||||
public async Task UpdateAccountInMemory(Account account, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
Uri url = account.serverInfo.migration?.movedTo ?? new(account.serverInfo.url);
|
||||
|
||||
ActiveUserServerInfoResponse userServerInfo = await accountFactory
|
||||
.GetUserServerInfo(url, account.token, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (userServerInfo.activeUser == null)
|
||||
{
|
||||
Uri url = new(account.serverInfo.url);
|
||||
var tokenResponse = await GetRefreshedToken(account.refreshToken, url, app).ConfigureAwait(false);
|
||||
account.token = tokenResponse.token;
|
||||
account.refreshToken = tokenResponse.refreshToken;
|
||||
account.isOnline = true;
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
account.isOnline = false;
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
account.userInfo = userServerInfo.activeUser;
|
||||
account.serverInfo = userServerInfo.serverInfo;
|
||||
//This is a bit gross, since id is not marked nullable
|
||||
//but this will force re-generate the id (e.g. if the user's email, or servers url has changed)
|
||||
account.id = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -412,325 +206,103 @@ public sealed class AccountManager(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local identifier for the specified account.
|
||||
/// Adds an account to local storage by prompting the user to log in via their browser.
|
||||
/// </summary>
|
||||
/// <param name="account">The account for which to retrieve the local identifier.</param>
|
||||
/// <returns>The local identifier for the specified account in the form of "SERVER_URL?u=USER_ID".</returns>
|
||||
/// <remarks>
|
||||
/// <inheritdoc cref="Account.GetLocalIdentifier"/>
|
||||
/// </remarks>
|
||||
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
public Uri? GetLocalIdentifierForAccount(Account account)
|
||||
{
|
||||
var identifier = account.GetLocalIdentifier();
|
||||
|
||||
// Validate account is stored locally
|
||||
var searchResult = GetAccountForLocalIdentifier(identifier);
|
||||
|
||||
return searchResult == null ? null : identifier;
|
||||
}
|
||||
|
||||
public async Task<UserInfo> Validate(Account account)
|
||||
{
|
||||
Uri server = new(account.serverInfo.url);
|
||||
return await GetUserInfo(account.token, server).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account that corresponds to the given local identifier.
|
||||
/// </summary>
|
||||
/// <param name="localIdentifier">The local identifier of the account.</param>
|
||||
/// <returns>The account that matches the local identifier, or null if no match is found.</returns>
|
||||
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
public Account? GetAccountForLocalIdentifier(Uri localIdentifier)
|
||||
{
|
||||
var searchResult = GetAccounts()
|
||||
.FirstOrDefault(acc =>
|
||||
{
|
||||
var id = acc.GetLocalIdentifier();
|
||||
return id == localIdentifier;
|
||||
});
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
private Uri EnsureCorrectServerUrl(Uri? server)
|
||||
{
|
||||
var localUrl = server;
|
||||
if (localUrl == null)
|
||||
{
|
||||
localUrl = GetDefaultServerUrl();
|
||||
logger.LogDebug("The provided server url was null or empty. Changed to the default url {serverUrl}", localUrl);
|
||||
}
|
||||
return localUrl;
|
||||
}
|
||||
|
||||
private void EnsureGetAccessCodeFlowIsSupported()
|
||||
{
|
||||
if (!HttpListener.IsSupported)
|
||||
{
|
||||
logger.LogError("HttpListener not supported");
|
||||
throw new PlatformNotSupportedException("Your operating system is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetAccessCode(Uri server, string challenge, TimeSpan timeout)
|
||||
{
|
||||
EnsureGetAccessCodeFlowIsSupported();
|
||||
|
||||
logger.LogDebug("Starting auth process for {server}/authn/verify/sca/{challenge}", server, challenge);
|
||||
|
||||
var accessCode = "";
|
||||
|
||||
Process.Start(new ProcessStartInfo($"{server}/authn/verify/sca/{challenge}") { UseShellExecute = true });
|
||||
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
using var listener = new HttpListener();
|
||||
var localUrl = "http://localhost:29363/";
|
||||
listener.Prefixes.Add(localUrl);
|
||||
listener.Start();
|
||||
logger.LogDebug("Listening for auth redirects on {localUrl}", localUrl);
|
||||
// Note: The GetContext method blocks while waiting for a request.
|
||||
HttpListenerContext context = listener.GetContext();
|
||||
HttpListenerRequest request = context.Request;
|
||||
HttpListenerResponse response = context.Response;
|
||||
|
||||
accessCode = request.QueryString["access_code"];
|
||||
logger.LogDebug("Got access code {accessCode}", accessCode);
|
||||
|
||||
string message =
|
||||
accessCode != null
|
||||
? "Success!<br/><br/>You can close this window now.<script>window.close();</script>"
|
||||
: "Oups, something went wrong...!";
|
||||
|
||||
var responseString =
|
||||
$"<HTML><BODY Style='background: linear-gradient(to top right, #ffffff, #c8e8ff); font-family: Roboto, sans-serif; font-size: 2rem; font-weight: 500; text-align: center;'><br/>{message}</BODY></HTML>";
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
|
||||
response.ContentLength64 = buffer.Length;
|
||||
Stream output = response.OutputStream;
|
||||
output.Write(buffer, 0, buffer.Length);
|
||||
output.Close();
|
||||
logger.LogDebug("Processed finished processing the access code");
|
||||
listener.Stop();
|
||||
listener.Close();
|
||||
});
|
||||
|
||||
var completedTask = await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false);
|
||||
|
||||
// this is means the task timed out
|
||||
if (completedTask != task)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Local auth flow failed to complete within the timeout window. Access code is {accessCode}",
|
||||
accessCode
|
||||
);
|
||||
throw new AuthFlowException("Local auth flow failed to complete within the timeout window");
|
||||
}
|
||||
|
||||
if (task.IsFaulted && task.Exception is not null)
|
||||
{
|
||||
logger.LogError(
|
||||
task.Exception,
|
||||
"Getting access code flow failed with {exceptionMessage}",
|
||||
task.Exception.Message
|
||||
);
|
||||
throw new AuthFlowException($"Auth flow failed: {task.Exception.Message}", task.Exception);
|
||||
}
|
||||
|
||||
// task completed within timeout
|
||||
logger.LogInformation(
|
||||
"Local auth flow completed successfully within the timeout window. Access code is {accessCode}",
|
||||
accessCode
|
||||
);
|
||||
return accessCode;
|
||||
}
|
||||
|
||||
private async Task<Account> CreateAccount(string accessCode, string challenge, Uri server)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenResponse = await GetToken(accessCode, challenge, server).ConfigureAwait(false);
|
||||
|
||||
var account = await accountFactory
|
||||
.CreateAccount(server, tokenResponse.token, tokenResponse.refreshToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
account.isDefault = !GetAccounts().Any();
|
||||
|
||||
logger.LogInformation("Successfully created account for {serverUrl}", server);
|
||||
|
||||
return account;
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleAccountManagerException("Failed to create account from access code and challenge", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryLockAccountAddFlow(TimeSpan timespan)
|
||||
{
|
||||
// use a static variable to quickly
|
||||
// prevent launching this flow multiple times
|
||||
if (s_isAddingAccount)
|
||||
{
|
||||
// this should probably throw with an error message
|
||||
throw new SpeckleAccountFlowLockedException("The account add flow is already launched.");
|
||||
}
|
||||
|
||||
// this uses the SQLite transport to store locks
|
||||
var lockIds = _accountAddLockStorage.GetAllObjects().Select(x => x.Id).OrderByDescending(d => d).ToList();
|
||||
var now = DateTime.Now;
|
||||
foreach (var l in lockIds)
|
||||
{
|
||||
var lockArray = l.Split('@');
|
||||
var lockName = lockArray.Length == 2 ? lockArray[0] : "the other app";
|
||||
var lockTime =
|
||||
lockArray.Length == 2
|
||||
? DateTime.ParseExact(lockArray[1], "o", null)
|
||||
: DateTime.ParseExact(lockArray[0], "o", null);
|
||||
|
||||
if (lockTime > now)
|
||||
{
|
||||
var lockString = string.Format("{0:mm} minutes {0:ss} seconds", lockTime - now);
|
||||
throw new SpeckleAccountFlowLockedException(
|
||||
$"The account add flow was already started in {lockName}, retry in {lockString}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var lockId = application.ApplicationAndVersion + "@" + DateTime.Now.Add(timespan).ToString("o");
|
||||
|
||||
// using the lock release time as an id and value
|
||||
// for ease of deletion and retrieval
|
||||
_accountAddLockStorage.SaveObject(lockId, lockId);
|
||||
s_isAddingAccount = true;
|
||||
}
|
||||
|
||||
private void UnlockAccountAddFlow()
|
||||
{
|
||||
s_isAddingAccount = false;
|
||||
// make sure all old locks are removed
|
||||
foreach (var (id, _) in _accountAddLockStorage.GetAllObjects())
|
||||
{
|
||||
_accountAddLockStorage.DeleteObject(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an account by propting the user to log in via a web flow
|
||||
/// </summary>
|
||||
/// <param name="server">Server to use to add the account, if not provied the default Server will be used</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// Account account = await AuthenticateAccount(new Uri("https://app.speckle.systems"), TimeSpan.FromMinutes(1));
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="serverUrl"></param>
|
||||
/// <param name="timeout">Timeout for user to auth with browser, recommend 1 min timeout</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task AddAccount(Uri? server = null)
|
||||
public async Task<Account> AuthenticateAccount(Uri serverUrl, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("Starting to add account for {serverUrl}", server);
|
||||
logger.LogDebug("Starting to add account for {ServerUrl}", serverUrl);
|
||||
|
||||
server = EnsureCorrectServerUrl(server);
|
||||
TokenExchangeResponse tokenResponse = await authFlow
|
||||
.TriggerAuthFlowWithTimeout(serverUrl, AuthApp.ConnectorsV3, timeout, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// locking for 1 minute
|
||||
var timeout = TimeSpan.FromMinutes(1);
|
||||
// this is not part of the try finally block
|
||||
// we do not want to clean up the existing locks
|
||||
TryLockAccountAddFlow(timeout);
|
||||
var challenge = GenerateChallenge();
|
||||
return await CreateAndAddAccount(serverUrl, tokenResponse, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
public async Task<Account> CreateAndAddAccount(
|
||||
Uri serverUrl,
|
||||
TokenExchangeResponse tokenResponse,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var account = await accountFactory
|
||||
.CreateAccount(serverUrl, tokenResponse.token, tokenResponse.refreshToken, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
account.isDefault = !GetAccounts().Any();
|
||||
|
||||
_accountStorage.SaveObject(account.id, JsonConvert.SerializeObject(account));
|
||||
logger.LogInformation("Successfully authenticated account {AccountId} for {ServerUrl}", account.id, serverUrl);
|
||||
return account;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Default Server URL for authentication, can be overridden by placing a file with the alternative url in the Speckle folder or with an ENV_VAR
|
||||
/// </summary>
|
||||
[Obsolete("Unused")]
|
||||
public Uri GetDefaultServerUrl()
|
||||
{
|
||||
var customServerUrl = "";
|
||||
|
||||
// first mechanism, check for local file
|
||||
var customServerFile = Path.Combine(SpecklePathProvider.UserSpeckleFolderPath, "server");
|
||||
if (File.Exists(customServerFile))
|
||||
{
|
||||
string accessCode = await GetAccessCode(server, challenge, timeout).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(accessCode))
|
||||
customServerUrl = File.ReadAllText(customServerFile);
|
||||
}
|
||||
|
||||
// second mechanism, check ENV VAR
|
||||
var customServerEnvVar = Environment.GetEnvironmentVariable("SPECKLE_SERVER");
|
||||
if (!string.IsNullOrEmpty(customServerEnvVar))
|
||||
{
|
||||
customServerUrl = customServerEnvVar;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(customServerUrl))
|
||||
{
|
||||
if (Uri.TryCreate(customServerUrl, UriKind.Absolute, out Uri? url))
|
||||
{
|
||||
throw new SpeckleAccountManagerException("Access code is invalid");
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
var account = await CreateAccount(accessCode, challenge, server).ConfigureAwait(false);
|
||||
|
||||
//if the account already exists it will not be added again
|
||||
_accountStorage.SaveObject(account.id, JsonConvert.SerializeObject(account));
|
||||
logger.LogDebug("Finished adding account {accountId} for {serverUrl}", account.id, server);
|
||||
}
|
||||
catch (SpeckleAccountManagerException ex)
|
||||
{
|
||||
logger.LogCritical(ex, "Failed to add account: {exceptionMessage}", ex.Message);
|
||||
// rethrowing any known errors
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
logger.LogCritical(ex, "Failed to add account: {exceptionMessage}", ex.Message);
|
||||
throw new SpeckleAccountManagerException($"Failed to add account: {ex.Message}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnlockAccountAddFlow();
|
||||
}
|
||||
return new Uri(DEFAULT_SERVER_URL);
|
||||
}
|
||||
|
||||
private async Task<TokenExchangeResponse> GetToken(string accessCode, string challenge, Uri server)
|
||||
[Obsolete("Use Uri overload")]
|
||||
public IEnumerable<Account> GetAccounts(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = speckleHttp.CreateHttpClient();
|
||||
|
||||
var body = new
|
||||
{
|
||||
appId = "sca",
|
||||
appSecret = "sca",
|
||||
accessCode,
|
||||
challenge,
|
||||
};
|
||||
|
||||
using var content = new StringContent(JsonConvert.SerializeObject(body));
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
var response = await client.PostAsync(new Uri(server, "/auth/token"), content).ConfigureAwait(false);
|
||||
|
||||
return JsonConvert
|
||||
.DeserializeObject<TokenExchangeResponse>(await response.Content.ReadAsStringAsync().ConfigureAwait(false))
|
||||
.NotNull();
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleException($"Failed to get authentication token from {server}", ex);
|
||||
}
|
||||
return GetAccounts(new Uri(serverUrl));
|
||||
}
|
||||
|
||||
private async Task<TokenExchangeResponse> GetRefreshedToken(string? refreshToken, Uri server, string app = "sca")
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = speckleHttp.CreateHttpClient();
|
||||
[Obsolete("Use UpdateAccount instead for more control over error handling", true)]
|
||||
public Task UpdateAccounts(CancellationToken ct = default, string app = "sca") => throw new NotImplementedException();
|
||||
|
||||
var body = new
|
||||
{
|
||||
appId = app,
|
||||
appSecret = app,
|
||||
refreshToken,
|
||||
};
|
||||
[Obsolete("Use UpdateAccount instead", true)]
|
||||
public void UpgradeAccount(string id) => throw new NotImplementedException();
|
||||
|
||||
using var content = new StringContent(JsonConvert.SerializeObject(body));
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
var response = await client.PostAsync(new Uri(server, "/auth/token"), content).ConfigureAwait(false);
|
||||
[Obsolete($"Use {nameof(AuthenticateAccount)} instead", true)]
|
||||
public Task AddAccount(Uri? server = null) => throw new NotImplementedException();
|
||||
|
||||
return JsonConvert
|
||||
.DeserializeObject<TokenExchangeResponse>(await response.Content.ReadAsStringAsync().ConfigureAwait(false))
|
||||
.NotNull();
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleException($"Failed to get refreshed token from {server}", ex);
|
||||
}
|
||||
}
|
||||
[Obsolete("Use serverInfo stored on a client instead", true)]
|
||||
public Task<ServerInfo> GetServerInfo(Uri server, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
private static string GenerateChallenge()
|
||||
{
|
||||
#if NET8_0
|
||||
byte[] challengeData = RandomNumberGenerator.GetBytes(32);
|
||||
#else
|
||||
using RNGCryptoServiceProvider rng = new();
|
||||
byte[] challengeData = new byte[32];
|
||||
rng.GetBytes(challengeData);
|
||||
#endif
|
||||
//escaped chars like % do not play nice with the server
|
||||
return Regex.Replace(Convert.ToBase64String(challengeData), @"[^\w\.@-]", "");
|
||||
}
|
||||
[Obsolete("Use userInfo stored on a client instead", true)]
|
||||
public Task<UserInfo> GetUserInfo(string token, Uri server, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
[Obsolete("Accounts must now be stored in sqlite db, no more json workaround", true)]
|
||||
public IList<Account> GetLocalAccounts() => throw new NotImplementedException();
|
||||
|
||||
[Obsolete("Use UpdateAccount or UpdateAccountInMemory Instead", true)]
|
||||
public IList<Account> Validate() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
public readonly record struct AuthApp(string AppId, string AppSecret, Uri CallbackUrl)
|
||||
{
|
||||
//These values are defined on the server, and specify the scopes the app is requesting
|
||||
public static AuthApp ConnectorsV3 { get; } =
|
||||
new()
|
||||
{
|
||||
AppId = "connectrV3",
|
||||
AppSecret = "connectrV3",
|
||||
CallbackUrl = new Uri("http://localhost:29355"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication flow with the Speckle Server to create a application token for the <c>connectorsV3</c> application
|
||||
/// Starts the browser based authentication flow where the user's browser will be opened, they'll be asked to
|
||||
/// confirm permission, then an access code will be given via a <see cref="HttpListener"/> which will be exchanged
|
||||
/// for a <see cref="TokenExchangeResponse"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note, this class is not coupled in any way to <see cref="Account"/>
|
||||
/// lets keep it that way...
|
||||
/// See instead <see cref="AccountManager"/>
|
||||
/// </remarks>
|
||||
[GenerateAutoInterface]
|
||||
public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp speckleHttp) : IAuthFlow
|
||||
{
|
||||
private readonly JsonSerializerSettings _serializerSettings = new()
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Error,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
};
|
||||
|
||||
public async Task<TokenExchangeResponse> TriggerAuthFlowWithTimeout(
|
||||
Uri serverUrl,
|
||||
AuthApp authApp,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using HttpClient client = speckleHttp.CreateHttpClient();
|
||||
|
||||
Uri tokenEndpoint = new(serverUrl, "/oauth/token");
|
||||
string codeVerifier = GenerateCodeVerifier();
|
||||
Uri authnVerify;
|
||||
using var req = await client.GetAsync(tokenEndpoint, cancellationToken).ConfigureAwait(false);
|
||||
bool useLegacyEndpoint = req.StatusCode != HttpStatusCode.OK;
|
||||
|
||||
if (useLegacyEndpoint)
|
||||
{
|
||||
string challenge = codeVerifier; // Old endpoint only supports PKCE "plain" method
|
||||
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}", UriKind.Relative);
|
||||
tokenEndpoint = new(serverUrl, "/auth/token");
|
||||
}
|
||||
else
|
||||
{
|
||||
string challenge = GenerateCodeChallenge(codeVerifier);
|
||||
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}?code_challenge_method=S256", UriKind.Relative);
|
||||
}
|
||||
|
||||
Uri endpoint = new(serverUrl, authnVerify);
|
||||
_ = Process.Start(new ProcessStartInfo(endpoint.ToString()) { UseShellExecute = true });
|
||||
string accessCode = await RunListenerWithTimeout(authApp.CallbackUrl, timeout, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
object body = useLegacyEndpoint
|
||||
? new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
appSecret = authApp.AppSecret,
|
||||
accessCode = accessCode,
|
||||
challenge = codeVerifier,
|
||||
}
|
||||
: new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
accessCode = accessCode,
|
||||
codeVerifier = codeVerifier,
|
||||
};
|
||||
|
||||
return await ExchangeAccessCodeForToken(
|
||||
client,
|
||||
JsonConvert.SerializeObject(body, _serializerSettings),
|
||||
tokenEndpoint,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="applicationCallbackUrl"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="userCancellation"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="OperationCanceledException"><paramref name="userCancellation"/> requested cancel</exception>
|
||||
/// <exception cref="TimeoutException">timeout was reached</exception>
|
||||
public async Task<string> RunListenerWithTimeout(
|
||||
Uri applicationCallbackUrl,
|
||||
TimeSpan timeout,
|
||||
CancellationToken userCancellation
|
||||
)
|
||||
{
|
||||
using CancellationTokenSource cancelOnTimeout = new(timeout);
|
||||
using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancelOnTimeout.Token,
|
||||
userCancellation
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
using var activity = activityFactory.Start("Listening for authflow access code");
|
||||
|
||||
return await RunListener(applicationCallbackUrl, linkedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (userCancellation.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancelOnTimeout.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException($"Auth flow was cancelled after {timeout:g} timeout", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="refreshToken"></param>
|
||||
/// <param name="serverUrl"></param>
|
||||
/// <param name="authApp">Auth app, needs to match the app that generated the refresh token originally</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="HttpRequestException">HTTP exceptions</exception>
|
||||
/// <exception cref="JsonSerializationException">Server response was invalid or partial</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException ">Invalid <paramref name="serverUrl"/> (must be absolute url)</exception>
|
||||
/// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> requested cancel</exception>
|
||||
/// <returns></returns>
|
||||
public async Task<TokenExchangeResponse> GetRefreshedToken(
|
||||
string? refreshToken,
|
||||
Uri serverUrl,
|
||||
AuthApp authApp,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var client = speckleHttp.CreateHttpClient();
|
||||
|
||||
var body = new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
appSecret = authApp.AppSecret,
|
||||
refreshToken = refreshToken,
|
||||
};
|
||||
|
||||
using var content = new StringContent(JsonConvert.SerializeObject(body, _serializerSettings));
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
var response = await client
|
||||
.PostAsync(new Uri(serverUrl, "/auth/token"), content, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
string read = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
string read = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
return JsonConvert.DeserializeObject<TokenExchangeResponse>(read, _serializerSettings).NotNull();
|
||||
}
|
||||
|
||||
private static async Task<HttpListenerContext> GetContext(HttpListener listener, CancellationToken cancellationToken)
|
||||
{
|
||||
//GetContextAsync doesn't support cancellation, so we have to do this song and dance...
|
||||
Task timeoutTask = Task.Delay(Timeout.Infinite, cancellationToken);
|
||||
Task<HttpListenerContext> getContextTask = listener.GetContextAsync();
|
||||
|
||||
Task completed = await Task.WhenAny(getContextTask, timeoutTask).ConfigureAwait(false);
|
||||
if (completed == getContextTask)
|
||||
{
|
||||
return getContextTask.Result;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
throw new InvalidOperationException("Cancellation should have thrown, this shouldn't be possible");
|
||||
}
|
||||
|
||||
public static async Task<string> RunListener(Uri localUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpListener listener = new();
|
||||
listener.Prefixes.Add(localUrl.ToString());
|
||||
listener.Start();
|
||||
|
||||
HttpListenerContext context = await GetContext(listener, cancellationToken).ConfigureAwait(false);
|
||||
HttpListenerRequest request = context.Request;
|
||||
using HttpListenerResponse response = context.Response;
|
||||
|
||||
string? accessCode = request.QueryString["access_code"];
|
||||
string? denied = request.QueryString["denied"];
|
||||
bool isDenied = denied == "true";
|
||||
|
||||
if (isDenied)
|
||||
{
|
||||
//lang=html
|
||||
WriteResponse(
|
||||
"""
|
||||
<h1>Denied!</h1>
|
||||
<br/><br/>
|
||||
Please close this window and return to your Speckle Connector.
|
||||
"""
|
||||
);
|
||||
throw new AuthFlowException("Authentication flow was denied"); //denied presumably by the user
|
||||
}
|
||||
else if (accessCode != null)
|
||||
{
|
||||
//lang=html
|
||||
WriteResponse(
|
||||
"""
|
||||
<h1>Success!</h1>
|
||||
<br/><br/>
|
||||
Your Speckle Connector is now authorized
|
||||
<br/><br/>
|
||||
You may now close this window and return to your Speckle Connector
|
||||
"""
|
||||
);
|
||||
return accessCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
//lang=html
|
||||
WriteResponse(
|
||||
"""
|
||||
<h1>Failed!</h1>
|
||||
<br/><br/>
|
||||
Something went wrong trying to authorize your Speckle Connector
|
||||
<br/><br/>
|
||||
Please close this window and try again from your Speckle Connector.
|
||||
"""
|
||||
);
|
||||
throw new AuthFlowException("Failed to receive access code");
|
||||
}
|
||||
|
||||
void WriteResponse(string message)
|
||||
{
|
||||
//lang=html
|
||||
string responseString = $"""
|
||||
<HTML>
|
||||
<BODY Style='background: #FAFAFAFF; font-family: Inter, Roboto, sans-serif; font-size: 1rem; font-weight: 500; text-align: center;'>
|
||||
<br/>
|
||||
{message}
|
||||
</BODY>
|
||||
</HTML>
|
||||
""";
|
||||
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
|
||||
response.ContentLength64 = buffer.Length;
|
||||
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TokenExchangeResponse> ExchangeAccessCodeForToken(
|
||||
HttpClient client,
|
||||
string jsonContent,
|
||||
Uri tokenEndpoint,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using StringContent content = new(jsonContent);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
|
||||
using HttpResponseMessage response = await client
|
||||
.PostAsync(tokenEndpoint, content, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
string read = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
string read = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
return JsonConvert.DeserializeObject<TokenExchangeResponse>(read, _serializerSettings).NotNull();
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string GenerateCodeVerifier()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Span<byte> codeVerifierData = stackalloc byte[32];
|
||||
RandomNumberGenerator.Fill(codeVerifierData);
|
||||
#else
|
||||
using RNGCryptoServiceProvider rng = new();
|
||||
byte[] codeVerifierData = new byte[32];
|
||||
rng.GetBytes(codeVerifierData);
|
||||
#endif
|
||||
|
||||
return Base64UrlEncode(codeVerifierData);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string GenerateCodeChallenge(string codeVerifier)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
int byteCount = Encoding.UTF8.GetByteCount(codeVerifier.AsSpan());
|
||||
Span<byte> codeVerifierBytes = stackalloc byte[byteCount];
|
||||
Encoding.UTF8.GetBytes(codeVerifier, codeVerifierBytes);
|
||||
Span<byte> challengeData = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(codeVerifierBytes, challengeData);
|
||||
#else
|
||||
byte[] codeVerifierBytes = Encoding.UTF8.GetBytes(codeVerifier);
|
||||
using SHA256 hash = SHA256.Create();
|
||||
byte[] challengeData = hash.ComputeHash(codeVerifierBytes);
|
||||
#endif
|
||||
return Base64UrlEncode(challengeData);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static string Base64UrlEncode(
|
||||
#if NET8_0_OR_GREATER
|
||||
ReadOnlySpan<byte> bytes
|
||||
#else
|
||||
byte[] bytes
|
||||
#endif
|
||||
)
|
||||
{
|
||||
// Base64Url is available in .NET 9, or via the Microsoft.Bcl.Memory polyfill
|
||||
// But for simplicity r.e. dll dependencies, we're doing it the dumb way...
|
||||
return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
#pragma warning disable CA2237
|
||||
public sealed class AuthFlowException : Exception
|
||||
#pragma warning restore CA2237
|
||||
{
|
||||
public AuthFlowException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
|
||||
public AuthFlowException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public AuthFlowException() { }
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
public sealed class AuthFlowException : SpeckleException
|
||||
{
|
||||
public AuthFlowException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
|
||||
public AuthFlowException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public AuthFlowException() { }
|
||||
}
|
||||
|
||||
public class SpeckleAccountManagerException : SpeckleException
|
||||
{
|
||||
public SpeckleAccountManagerException(string message)
|
||||
@@ -10,14 +21,3 @@ public class SpeckleAccountManagerException : SpeckleException
|
||||
|
||||
public SpeckleAccountManagerException() { }
|
||||
}
|
||||
|
||||
public class SpeckleAccountFlowLockedException : SpeckleAccountManagerException
|
||||
{
|
||||
public SpeckleAccountFlowLockedException(string message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleAccountFlowLockedException() { }
|
||||
|
||||
public SpeckleAccountFlowLockedException(string message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
@@ -6,16 +6,19 @@ namespace Speckle.Sdk.Credentials;
|
||||
internal sealed class ActiveUserServerInfoResponse
|
||||
{
|
||||
[property: JsonProperty(Required = Required.AllowNull)]
|
||||
public UserInfo? activeUser { get; init; }
|
||||
public required UserInfo? activeUser { get; init; }
|
||||
|
||||
[property: JsonProperty(Required = Required.Always)]
|
||||
public ServerInfo serverInfo { get; init; }
|
||||
public required ServerInfo serverInfo { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class TokenExchangeResponse
|
||||
public sealed class TokenExchangeResponse
|
||||
{
|
||||
public string token { get; init; }
|
||||
public string refreshToken { get; init; }
|
||||
[JsonRequired]
|
||||
public required string token { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string refreshToken { get; init; }
|
||||
}
|
||||
|
||||
public sealed class UserInfo
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Speckle.Sdk.Helpers;
|
||||
|
||||
public static class BlobApiHelpers
|
||||
{
|
||||
public static string ParseEtagHeader(HttpResponseHeaders headers)
|
||||
{
|
||||
if (!headers.TryGetValues("ETag", out var etagValues))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Response does not have an ETag attached to it, cannot use this as an upload",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
var etagValuesArray = etagValues.ToArray();
|
||||
|
||||
if (etagValuesArray.Length != 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected Etag header to have a single value but got {etagValuesArray.Length}",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
return etagValuesArray[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IDisposable"/> wrapper around the downloaded file to try and delete the file on Dispose
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We're using a similar pattern in the Rhino File Importer codebase (see <c>ImportJobFile</c>)
|
||||
/// </remarks>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="file"></param>
|
||||
public sealed class DisposableFile(FileInfo file, ILogger logger, bool deleteOnDispose = true) : IDisposable
|
||||
{
|
||||
public FileInfo FileInfo => file;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!deleteOnDispose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
logger.LogInformation("Cleaned up {File}", file);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to clean up {File}", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Speckle.Sdk.Helpers;
|
||||
|
||||
public static class StopwatchPolyfills
|
||||
{
|
||||
#if !NET7_0_OR_GREATER
|
||||
private static readonly double s_tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
|
||||
#endif
|
||||
|
||||
public static TimeSpan GetElapsedTime(long startingTimestamp)
|
||||
{
|
||||
#if NET7_0_OR_GREATER
|
||||
return Stopwatch.GetElapsedTime(startingTimestamp);
|
||||
#else
|
||||
|
||||
long elapsedTicks = Stopwatch.GetTimestamp() - startingTimestamp;
|
||||
return new TimeSpan((long)(elapsedTicks * s_tickFrequency));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,22 @@ internal static class TypeLoader
|
||||
return typeof(Base);
|
||||
}
|
||||
|
||||
//Don't use unless you're testing
|
||||
/// <summary>
|
||||
/// For testing purposes only
|
||||
/// </summary>
|
||||
internal static void ReInitialize(params Assembly[] assemblies)
|
||||
{
|
||||
lock (s_availableTypes)
|
||||
{
|
||||
Reset();
|
||||
Load(assemblies);
|
||||
s_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For testing purposes only
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
s_availableTypes = new();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
namespace Speckle.Sdk.Logging;
|
||||
using Speckle.Connectors.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Logging;
|
||||
|
||||
public sealed class NullActivityFactory : ISdkActivityFactory
|
||||
{
|
||||
public void Dispose() { }
|
||||
|
||||
public ISdkActivity? Start(string? name = default, string source = "") => null;
|
||||
public ISdkActivity? Start(string? name, SdkActivityKind kind, string source) => null;
|
||||
|
||||
public ISdkActivity? StartRemote(string traceContext, SdkActivityKind kind, string? name, string source) => null;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ public sealed class DetachPropertyAttribute : Attribute
|
||||
/// <para>If set to true the default serialiser will persist it separately, and add a reference to the property's value in the original object.</para>
|
||||
/// <para>Only applies to properties of types derived from the Base class.</para>
|
||||
/// </summary>
|
||||
/// <param name="detachable">Whether to detach the property or not.</param>
|
||||
public DetachPropertyAttribute() { }
|
||||
|
||||
[Obsolete("detachable = false is no longer supported")]
|
||||
public DetachPropertyAttribute(bool detachable = true)
|
||||
{
|
||||
Detachable = detachable;
|
||||
|
||||
@@ -7,12 +7,12 @@ namespace Speckle.Sdk.Models;
|
||||
public enum DynamicBaseMemberType
|
||||
{
|
||||
/// <summary>
|
||||
/// The typed members of the DynamicBase object
|
||||
/// The typed members of the <see cref="DynamicBase"/> object
|
||||
/// </summary>
|
||||
Instance = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The dynamically added members of the DynamicBase object
|
||||
/// The dynamically added members of the <see cref="DynamicBase"/> object
|
||||
/// </summary>
|
||||
Dynamic = 2,
|
||||
|
||||
@@ -22,8 +22,9 @@ public enum DynamicBaseMemberType
|
||||
Obsolete = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The typed methods flagged with TODO:
|
||||
/// Old feature supported in v2 for grasshopper
|
||||
/// </summary>
|
||||
[Obsolete("Feature no longer supported")]
|
||||
SchemaComputed = 16,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
public sealed class AggregateProgress<T>(params IProgress<T>[] progresses) : IProgress<T>
|
||||
{
|
||||
public void Report(T value)
|
||||
{
|
||||
foreach (var progress in progresses)
|
||||
{
|
||||
progress.Report(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Helpers;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
public partial interface IIngestionProgressManager : IProgress<CardProgress>;
|
||||
|
||||
/// <summary>
|
||||
/// An <see langword="IProgress{IngestionProgressEventArgs}"/> implementation for the entire client side Ingestion progress update reporting
|
||||
/// Will throttles ingestion progress messages and reports their progress
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Normally we would pick quite a coarse updateInterval to try and spamming the server (1-5s)
|
||||
/// </remarks>
|
||||
[GenerateAutoInterface]
|
||||
public sealed class IngestionProgressManager(
|
||||
ILogger<IngestionProgressManager> logger,
|
||||
IClient speckleClient,
|
||||
ModelIngestion ingestion,
|
||||
TimeSpan updateInterval,
|
||||
CancellationToken cancellationToken
|
||||
) : IIngestionProgressManager
|
||||
{
|
||||
public Task? LastUpdate { get; private set; }
|
||||
|
||||
private long _lastUpdatedAt;
|
||||
private readonly object _lock = new();
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public void Report(CardProgress value)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string trimmedMessage;
|
||||
lock (_lock)
|
||||
{
|
||||
if (ShouldIgnoreProgressUpdate())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastUpdatedAt = Stopwatch.GetTimestamp();
|
||||
|
||||
trimmedMessage = value.Status.TrimEnd('.');
|
||||
|
||||
LastUpdate = speckleClient
|
||||
.Ingestion.UpdateProgress(
|
||||
new ModelIngestionUpdateInput(ingestion.id, ingestion.projectId, trimmedMessage, value.Progress),
|
||||
cancellationToken
|
||||
)
|
||||
.ContinueWith(
|
||||
Continuation,
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
}
|
||||
|
||||
logger.LogInformation("Progress update {Message} {Progress}", trimmedMessage, value.Progress);
|
||||
}
|
||||
|
||||
/// <returns><see langword="true"/> if the update should be ignored, otherwise <see langword="false"/></returns>
|
||||
private bool ShouldIgnoreProgressUpdate()
|
||||
{
|
||||
if (LastUpdate is not null && !LastUpdate.IsCompleted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TimeSpan msSinceLastUpdate = StopwatchPolyfills.GetElapsedTime(_lastUpdatedAt);
|
||||
return msSinceLastUpdate < updateInterval;
|
||||
}
|
||||
|
||||
private void Continuation(Task updateTask)
|
||||
{
|
||||
// The progress report failed... could be many reasons.
|
||||
// For now, we're not letting this fail the Ingestion in any way
|
||||
// we'll log but otherwise let it slide while leaving no unobserved task exceptions
|
||||
if (updateTask.IsFaulted)
|
||||
{
|
||||
logger.LogWarning(updateTask.Exception, "A progress update failed unexpectedly");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class IngestionProgressManagerFactory(ILogger<IngestionProgressManager> logger)
|
||||
: IIngestionProgressManagerFactory
|
||||
{
|
||||
public IIngestionProgressManager CreateInstance(
|
||||
IClient speckleClient,
|
||||
ModelIngestion ingestion,
|
||||
TimeSpan updateInterval,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
return new IngestionProgressManager(logger, speckleClient, ingestion, updateInterval, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
//TODO: rename PipelineProgressArgs
|
||||
public readonly record struct CardProgress(string Status, double? Progress);
|
||||
|
||||
public readonly record struct StreamProgressArgs(long BytesStreamed, long ExpectedTotalBytes);
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps <paramref name="innerStream"/> to report streaming progress as bytes are read/written.
|
||||
/// </summary>
|
||||
public sealed class ProgressStream(Stream innerStream, IProgress<StreamProgressArgs>? progress = null) : Stream
|
||||
{
|
||||
private long _bytesStreamed;
|
||||
|
||||
public override bool CanRead => innerStream.CanRead;
|
||||
public override bool CanSeek => innerStream.CanSeek;
|
||||
public override bool CanWrite => innerStream.CanWrite;
|
||||
public override long Length => innerStream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => innerStream.Position;
|
||||
set => innerStream.Position = value;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int bytesRead = innerStream.Read(buffer, offset, count);
|
||||
ReportProgress(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
[SuppressMessage(
|
||||
"Performance",
|
||||
"CA1835:Prefer the \'Memory\'-based overloads for \'ReadAsync\' and \'WriteAsync\'",
|
||||
Justification = "Analyser warning forwarded to caller"
|
||||
)]
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int bytesRead = await innerStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
ReportProgress(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int bytesRead = await innerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
ReportProgress(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ReportProgress(int newBytesProcessed)
|
||||
{
|
||||
_bytesStreamed += newBytesProcessed;
|
||||
progress?.Report(new(_bytesStreamed, Length));
|
||||
}
|
||||
|
||||
public override void Flush() => innerStream.Flush();
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) => innerStream.FlushAsync(cancellationToken);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => innerStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException(); //intentionally not supporting, as changing length of stream mid-flight will fuck up progress
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
innerStream.Write(buffer, offset, count);
|
||||
ReportProgress(count);
|
||||
}
|
||||
|
||||
[SuppressMessage(
|
||||
"Performance",
|
||||
"CA1835:Prefer the \'Memory\'-based overloads for \'ReadAsync\' and \'WriteAsync\'",
|
||||
Justification = "Analyser warning forwarded to caller"
|
||||
)]
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
await innerStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
ReportProgress(count);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await innerStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
ReportProgress(buffer.Length);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
innerStream.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await innerStream.DisposeAsync().ConfigureAwait(false);
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
/// <summary>
|
||||
/// Renders "low level" data stream updates
|
||||
/// into "high level" <see cref="CardProgress"/> that is expected by Ingestion progress and DUI3
|
||||
/// </summary>
|
||||
/// <param name="progress"></param>
|
||||
public sealed class RenderedStreamProgress(IProgress<CardProgress> progress) : IProgress<StreamProgressArgs>
|
||||
{
|
||||
public void Report(StreamProgressArgs value)
|
||||
{
|
||||
var (suffix, scaleFactor) = GetFileSizeRendering(value.ExpectedTotalBytes);
|
||||
progress.Report(
|
||||
new(
|
||||
$"Uploading data... ({value.BytesStreamed * scaleFactor:F1}/{value.ExpectedTotalBytes * scaleFactor:F1} {suffix})",
|
||||
(double)value.BytesStreamed / value.ExpectedTotalBytes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static readonly string[] s_suffixes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
internal static (string suffix, double scaleFactor) GetFileSizeRendering(long value)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
return (s_suffixes[0], 1d);
|
||||
}
|
||||
|
||||
for (int i = 0; i < s_suffixes.Length; i++)
|
||||
{
|
||||
if (value <= Math.Pow(1024, i + 1))
|
||||
{
|
||||
return (s_suffixes[i], 1 / Math.Pow(1024, i));
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Value is too large to convert to a file size");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Dependencies;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Send;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class DiskStoreFactory(ILogger<DiskStore> logger, ISdkActivityFactory activityFactory) : IDiskStoreFactory
|
||||
{
|
||||
public DiskStore CreateInstance(CancellationToken cancellationToken) =>
|
||||
new(logger, activityFactory, cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class DiskStore
|
||||
{
|
||||
private readonly RepackedChannel<UploadItem> _channel;
|
||||
private readonly Task<DisposableFile> _writeToDiskTask;
|
||||
private readonly ILogger<DiskStore> _logger;
|
||||
private readonly ISdkActivityFactory _activityFactory;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
internal DiskStore(
|
||||
ILogger<DiskStore> logger,
|
||||
ISdkActivityFactory activityFactory,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_activityFactory = activityFactory;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
_channel = new RepackedChannel<UploadItem>(1000, true, false);
|
||||
_writeToDiskTask = Task.Run(WriteFile, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task PushAsync(UploadItem item) =>
|
||||
await _channel.WriteAsync(item, _cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task<DisposableFile> CompleteAsync()
|
||||
{
|
||||
using var a = _activityFactory.Start("Waiting for DiskStore to complete");
|
||||
_channel.CompleteWriter();
|
||||
return await _writeToDiskTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private readonly byte[] _newLineChar = [(byte)'\n'];
|
||||
private readonly byte[] _tabLineChar = [(byte)'\t'];
|
||||
|
||||
/// <summary>
|
||||
/// Reads from the Channel and streams the <see cref="UploadItem"/>s to a temporary file on disk.
|
||||
/// Will keep reading until <see cref="CompleteAsync"/> is called.
|
||||
/// </summary>
|
||||
/// <returns>the file that was written</returns>
|
||||
private async Task<DisposableFile> WriteFile()
|
||||
{
|
||||
string tempFilePath = Path.GetTempFileName();
|
||||
var tempFile = new DisposableFile(new FileInfo(tempFilePath), _logger);
|
||||
_logger.LogInformation("Writing temp file to {TempFilePath}", tempFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
using var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
using var gzip = new GZipStream(fileStream, CompressionLevel.Optimal);
|
||||
|
||||
await foreach (var item in _channel.ReadAllAsync(_cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await gzip.WriteAsync(Encoding.UTF8.GetBytes(item.Id), _cancellationToken).ConfigureAwait(false);
|
||||
await gzip.WriteAsync(_tabLineChar, _cancellationToken).ConfigureAwait(false);
|
||||
await gzip.WriteAsync(Encoding.UTF8.GetBytes(item.SpeckleType), _cancellationToken).ConfigureAwait(false);
|
||||
await gzip.WriteAsync(_tabLineChar, _cancellationToken).ConfigureAwait(false);
|
||||
await gzip.WriteAsync(item.Json.WrittenMemory, _cancellationToken).ConfigureAwait(false);
|
||||
await gzip.WriteAsync(_newLineChar, _cancellationToken).ConfigureAwait(false);
|
||||
|
||||
item.Dispose();
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
await gzip.FlushAsync(_cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await gzip.FlushAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
tempFile.FileInfo.Refresh();
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
catch
|
||||
{
|
||||
tempFile.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Send;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class SendPipelineFactory(IUploaderFactory uploaderFactory, IDiskStoreFactory diskStoreFactory)
|
||||
: ISendPipelineFactory
|
||||
{
|
||||
public SendPipeline CreateInstance(
|
||||
string projectId,
|
||||
string ingestionId,
|
||||
Account account,
|
||||
IProgress<StreamProgressArgs> uploadProgress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var uploader = uploaderFactory.CreateInstance(projectId, ingestionId, account, uploadProgress, cancellationToken);
|
||||
var diskStore = diskStoreFactory.CreateInstance(cancellationToken);
|
||||
return new SendPipeline(uploader, diskStore);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SendPipeline : IDisposable
|
||||
{
|
||||
private readonly Serializer _serializer = new();
|
||||
private readonly Uploader _uploader;
|
||||
private readonly DiskStore _diskStore;
|
||||
|
||||
internal SendPipeline(Uploader uploader, DiskStore diskStore)
|
||||
{
|
||||
_uploader = uploader;
|
||||
_diskStore = diskStore;
|
||||
}
|
||||
|
||||
public async Task<ObjectReference> Process(Base @base)
|
||||
{
|
||||
var results = _serializer.Serialize(@base).ToArray();
|
||||
foreach (var item in results.Reverse())
|
||||
{
|
||||
// we're not doing fire and forget here so that we get the backpressure from the uploader
|
||||
await _diskStore.PushAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return results.First().Reference;
|
||||
}
|
||||
|
||||
public async Task WaitForUpload()
|
||||
{
|
||||
//await _diskStore.PushAsync(_lastItem).ConfigureAwait(false);
|
||||
using DisposableFile tempFile = await _diskStore.CompleteAsync().ConfigureAwait(false);
|
||||
|
||||
using Stream fileStreamUpload = new FileStream(
|
||||
tempFile.FileInfo.FullName,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read
|
||||
);
|
||||
|
||||
await _uploader.Send(fileStreamUpload).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose() => _uploader.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Text.Json;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
using Matrix4x4 = Speckle.DoubleNumerics.Matrix4x4;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Another serializer, cleaner and meaner. Provides methods for serializing Speckle objects into a format suitable for upload or storage.
|
||||
/// This class handles the conversion of <see cref="Speckle.Sdk.Models.Base"/> and its derivatives
|
||||
/// into serialized JSON structures along with associated metadata, closures, and references.
|
||||
/// <para>Any reference objects coming through are being "passed through" serialized - they do not get double encoded.</para>
|
||||
/// </summary>
|
||||
internal sealed class Serializer
|
||||
{
|
||||
private readonly record struct PropertyInfo(string Name, object? Value, bool IsDetachable);
|
||||
|
||||
public IEnumerable<UploadItem> Serialize(Base root)
|
||||
{
|
||||
// Special case: if root is already an ObjectReference, serialize it verbatim
|
||||
if (root is ObjectReference existingRef)
|
||||
{
|
||||
var uploadItem = ReferenceToUploadItem(existingRef);
|
||||
yield return uploadItem;
|
||||
yield break;
|
||||
}
|
||||
|
||||
var detachedObjects = new List<(Id, EfficientJson, Dictionary<string, int>, Base, string)>();
|
||||
var rootClosures = new Dictionary<string, int>();
|
||||
|
||||
var (rootId, rootJson) = SerializeBase(root, false, rootClosures, detachedObjects);
|
||||
|
||||
var rootReference = new ObjectReference
|
||||
{
|
||||
referencedId = rootId.Value,
|
||||
applicationId = root.applicationId,
|
||||
closure = rootClosures.Count > 0 ? rootClosures : null,
|
||||
};
|
||||
|
||||
yield return new UploadItem(rootId.Value, rootJson, root.speckle_type, rootReference);
|
||||
|
||||
foreach (var (id, json, closures, baseObj, speckleType) in detachedObjects)
|
||||
{
|
||||
var reference = new ObjectReference
|
||||
{
|
||||
referencedId = id.Value,
|
||||
applicationId = baseObj.applicationId,
|
||||
closure = closures.Count > 0 ? closures : null,
|
||||
};
|
||||
|
||||
yield return new UploadItem(id.Value, json, speckleType, reference);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<PropertyInfo> ExtractProperties(Base baseObj)
|
||||
{
|
||||
var typedProperties = baseObj.GetInstanceMembers();
|
||||
foreach (var prop in typedProperties)
|
||||
{
|
||||
if (prop.Name == "id" || prop.Name.StartsWith("__"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop.IsDefined(typeof(Speckle.Newtonsoft.Json.JsonIgnoreAttribute), false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = prop.GetValue(baseObj);
|
||||
var isDetachable = prop.IsDefined(typeof(DetachPropertyAttribute), true);
|
||||
|
||||
yield return new PropertyInfo(prop.Name, value, isDetachable);
|
||||
}
|
||||
|
||||
foreach (var propName in baseObj.DynamicPropertyKeys)
|
||||
{
|
||||
if (propName.StartsWith("__"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = baseObj[propName];
|
||||
|
||||
#pragma warning disable CA1866
|
||||
var isDetachable = propName.StartsWith("@");
|
||||
#pragma warning restore CA1866
|
||||
|
||||
yield return new PropertyInfo(propName, value, isDetachable);
|
||||
}
|
||||
}
|
||||
|
||||
private (Id, EfficientJson) SerializeBase(
|
||||
Base baseObj,
|
||||
bool forceDetach,
|
||||
Dictionary<string, int> closures,
|
||||
List<(Id, EfficientJson, Dictionary<string, int>, Base, string)> detachedObjects
|
||||
)
|
||||
{
|
||||
var childClosures = new Dictionary<string, int>();
|
||||
|
||||
var efficientJson = new EfficientJson();
|
||||
using var jsonWriter = new Utf8JsonWriter(efficientJson.Buffer);
|
||||
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
foreach (var prop in ExtractProperties(baseObj))
|
||||
{
|
||||
jsonWriter.WritePropertyName(prop.Name);
|
||||
SerializeValue(prop.Value, jsonWriter, prop.IsDetachable, childClosures, detachedObjects);
|
||||
}
|
||||
|
||||
jsonWriter.Flush();
|
||||
var span = efficientJson.WrittenSpan;
|
||||
string id = IdGenerator.ComputeId(span);
|
||||
|
||||
jsonWriter.WriteString("id", id);
|
||||
|
||||
baseObj.id = id;
|
||||
|
||||
if ((forceDetach || childClosures.Count > 0) && childClosures.Count > 0)
|
||||
{
|
||||
jsonWriter.WritePropertyName("__closure");
|
||||
jsonWriter.WriteStartObject();
|
||||
foreach (var kvp in childClosures)
|
||||
{
|
||||
jsonWriter.WriteNumber(kvp.Key, kvp.Value);
|
||||
}
|
||||
jsonWriter.WriteEndObject();
|
||||
|
||||
foreach (var kvp in childClosures)
|
||||
{
|
||||
closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existing) ? existing + kvp.Value : kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
return (new(id), efficientJson);
|
||||
}
|
||||
|
||||
private void SerializeValue(
|
||||
object? value,
|
||||
Utf8JsonWriter writer,
|
||||
bool isDetachable,
|
||||
Dictionary<string, int> closures,
|
||||
List<(Id, EfficientJson, Dictionary<string, int>, Base, string)> detachedObjects
|
||||
)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
case string v:
|
||||
writer.WriteStringValue(v);
|
||||
return;
|
||||
case short i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case ushort i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case int i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case uint i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case long i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case ulong i:
|
||||
writer.WriteNumberValue(i);
|
||||
return;
|
||||
case bool b:
|
||||
writer.WriteBooleanValue(b);
|
||||
return;
|
||||
case float f:
|
||||
writer.WriteNumberValue(f);
|
||||
return;
|
||||
case double f:
|
||||
writer.WriteNumberValue(f);
|
||||
return;
|
||||
case decimal d:
|
||||
writer.WriteNumberValue(d);
|
||||
return;
|
||||
case Enum:
|
||||
writer.WriteNumberValue((int)value);
|
||||
return;
|
||||
case Color c:
|
||||
writer.WriteNumberValue(c.ToArgb());
|
||||
return;
|
||||
case Matrix4x4 md:
|
||||
writer.WriteStartArray();
|
||||
writer.WriteNumberValue(md.M11);
|
||||
writer.WriteNumberValue(md.M12);
|
||||
writer.WriteNumberValue(md.M13);
|
||||
writer.WriteNumberValue(md.M14);
|
||||
writer.WriteNumberValue(md.M21);
|
||||
writer.WriteNumberValue(md.M22);
|
||||
writer.WriteNumberValue(md.M23);
|
||||
writer.WriteNumberValue(md.M24);
|
||||
writer.WriteNumberValue(md.M31);
|
||||
writer.WriteNumberValue(md.M32);
|
||||
writer.WriteNumberValue(md.M33);
|
||||
writer.WriteNumberValue(md.M34);
|
||||
writer.WriteNumberValue(md.M41);
|
||||
writer.WriteNumberValue(md.M42);
|
||||
writer.WriteNumberValue(md.M43);
|
||||
writer.WriteNumberValue(md.M44);
|
||||
writer.WriteEndArray();
|
||||
return;
|
||||
// Handle ObjectReference before Base (since ObjectReference extends Base)
|
||||
// This prevents double-serialization and properly propagates closures
|
||||
case ObjectReference objRef:
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("speckle_type", "reference");
|
||||
writer.WriteString("referencedId", objRef.referencedId);
|
||||
writer.WriteEndObject();
|
||||
|
||||
// Propagate closure: add the referenced ID
|
||||
closures[objRef.referencedId] = closures.TryGetValue(objRef.referencedId, out var existing) ? existing + 1 : 1;
|
||||
|
||||
// Propagate nested closures from the ObjectReference.closure dictionary
|
||||
if (objRef.closure != null)
|
||||
{
|
||||
foreach (var kvp in objRef.closure)
|
||||
{
|
||||
closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth)
|
||||
? existingDepth + kvp.Value
|
||||
: kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case Base baseObj:
|
||||
{
|
||||
if (isDetachable)
|
||||
{
|
||||
var childClosures = new Dictionary<string, int>();
|
||||
var (childId, childJson) = SerializeBase(baseObj, true, childClosures, detachedObjects);
|
||||
|
||||
detachedObjects.Add((childId, childJson, childClosures, baseObj, baseObj.speckle_type));
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("speckle_type", "reference");
|
||||
writer.WriteString("referencedId", childId.Value);
|
||||
writer.WriteEndObject();
|
||||
|
||||
closures[childId.Value] = closures.TryGetValue(childId.Value, out var existing) ? existing + 1 : 1;
|
||||
|
||||
foreach (var kvp in childClosures)
|
||||
{
|
||||
closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth)
|
||||
? existingDepth + kvp.Value
|
||||
: kvp.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var inlineClosures = new Dictionary<string, int>();
|
||||
var (_, inlineJson) = SerializeBase(baseObj, false, inlineClosures, detachedObjects);
|
||||
|
||||
writer.WriteRawValue(inlineJson.WrittenSpan);
|
||||
|
||||
foreach (var kvp in inlineClosures)
|
||||
{
|
||||
closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth)
|
||||
? existingDepth + kvp.Value
|
||||
: kvp.Value;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case IDictionary dict:
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (DictionaryEntry kvp in dict)
|
||||
{
|
||||
if (kvp.Key is not string key)
|
||||
{
|
||||
throw new ArgumentException("Dictionary keys must be strings", nameof(value));
|
||||
}
|
||||
|
||||
writer.WritePropertyName(key);
|
||||
SerializeValue(kvp.Value, writer, false, closures, detachedObjects);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
return;
|
||||
}
|
||||
case ICollection collection:
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
SerializeValue(item, writer, isDetachable, closures, detachedObjects);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
// This case will handle primitives and `null`
|
||||
// Will throw JsonWriterException if not supported
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type {value.GetType()}");
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage(
|
||||
"Reliability",
|
||||
"CA2000:Dispose objects before losing scope",
|
||||
Justification = "EfficientJson IDisposable is returned via UploadItem"
|
||||
)]
|
||||
private UploadItem ReferenceToUploadItem(ObjectReference existingRef)
|
||||
{
|
||||
var refJson = new EfficientJson();
|
||||
using var jsonWriter = new Utf8JsonWriter(refJson.Buffer);
|
||||
|
||||
jsonWriter.WriteStartObject();
|
||||
jsonWriter.WriteString("speckle_type", "reference");
|
||||
jsonWriter.WriteString("referencedId", existingRef.referencedId);
|
||||
jsonWriter.WritePropertyName("__closure");
|
||||
|
||||
if (existingRef.closure != null && existingRef.closure.Count > 0)
|
||||
{
|
||||
jsonWriter.WriteStartObject();
|
||||
foreach (var kvp in existingRef.closure)
|
||||
{
|
||||
jsonWriter.WritePropertyName(kvp.Key);
|
||||
jsonWriter.WriteNumberValue(kvp.Value);
|
||||
}
|
||||
jsonWriter.WriteEndObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
jsonWriter.WriteNullValue();
|
||||
}
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
|
||||
return new UploadItem(
|
||||
existingRef.referencedId,
|
||||
refJson,
|
||||
existingRef.speckle_type,
|
||||
existingRef // Pass through the original ObjectReference
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Send;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class UploaderFactory(ISpeckleHttp httpClientFactory, ISdkActivityFactory activityFactory)
|
||||
: IUploaderFactory
|
||||
{
|
||||
public Uploader CreateInstance(
|
||||
string projectId,
|
||||
string ingestionId,
|
||||
Account account,
|
||||
IProgress<StreamProgressArgs> progress,
|
||||
CancellationToken cancellationToken
|
||||
) => new(projectId, ingestionId, activityFactory, httpClientFactory, account, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class Uploader : IDisposable
|
||||
{
|
||||
private readonly string _projectId;
|
||||
private readonly string _ingestionId;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly HttpClient _speckleClient;
|
||||
private readonly HttpClient _s3Client;
|
||||
private readonly ISdkActivityFactory _activity;
|
||||
private readonly IProgress<StreamProgressArgs> _progress;
|
||||
|
||||
internal Uploader(
|
||||
string projectId,
|
||||
string ingestionId,
|
||||
ISdkActivityFactory activity,
|
||||
ISpeckleHttp httpClientFactory,
|
||||
Account speckleAccount,
|
||||
IProgress<StreamProgressArgs> progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
_projectId = projectId;
|
||||
_ingestionId = ingestionId;
|
||||
_activity = activity;
|
||||
_cancellationToken = cancellationToken;
|
||||
_progress = progress;
|
||||
_speckleClient = httpClientFactory.CreateHttpClient(authorizationToken: speckleAccount.token);
|
||||
_speckleClient.BaseAddress = new(new(speckleAccount.serverInfo.url), "/api/v1/");
|
||||
|
||||
_s3Client = httpClientFactory.CreateHttpClient();
|
||||
}
|
||||
|
||||
public async Task Send(Stream fileStream)
|
||||
{
|
||||
PresignedUploadResponse presignedUploadResponse = await GetPresignedUrl().ConfigureAwait(false);
|
||||
var etag = await UploadToS3(fileStream, presignedUploadResponse).ConfigureAwait(false);
|
||||
|
||||
await TriggerProcessing(new() { Etag = etag }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<PresignedUploadResponse> GetPresignedUrl()
|
||||
{
|
||||
using var a = _activity.Start("Get Presigned Url");
|
||||
|
||||
try
|
||||
{
|
||||
var signUri = new Uri($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/sign", UriKind.Relative);
|
||||
|
||||
using var signResponse = await _speckleClient.PostAsync(signUri, null, _cancellationToken).ConfigureAwait(false);
|
||||
signResponse.EnsureSuccessStatusCode();
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
string signResponseString = await signResponse
|
||||
.Content.ReadAsStringAsync(_cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
string signResponseString = await signResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
PresignedUploadResponse presignedUpload =
|
||||
JsonConvert.DeserializeObject<PresignedUploadResponse>(signResponseString)
|
||||
?? throw new InvalidOperationException("Failed to get presigned upload URL");
|
||||
return presignedUpload;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
a?.SetStatus(SdkActivityStatusCode.Error);
|
||||
a?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> UploadToS3(Stream fileStream, PresignedUploadResponse presignedUploadResponse)
|
||||
{
|
||||
using var a = _activity.Start("Uploading file to pre-signed url");
|
||||
try
|
||||
{
|
||||
Stream progressStream = new ProgressStream(fileStream, _progress);
|
||||
|
||||
using var streamContent = new StreamContent(progressStream);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
streamContent.Headers.ContentLength = fileStream.Length;
|
||||
|
||||
using var uploadRequest = new HttpRequestMessage(HttpMethod.Put, presignedUploadResponse.Url);
|
||||
foreach (var kvp in presignedUploadResponse.AdditionalRequestHeaders)
|
||||
{
|
||||
uploadRequest.Headers.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
uploadRequest.Content = streamContent;
|
||||
|
||||
using var uploadResponse = await _s3Client
|
||||
.SendAsync(uploadRequest, HttpCompletionOption.ResponseHeadersRead, _cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
uploadResponse.EnsureSuccessStatusCode();
|
||||
|
||||
return BlobApiHelpers.ParseEtagHeader(uploadResponse.Headers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
a?.SetStatus(SdkActivityStatusCode.Error);
|
||||
a?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TriggerProcessing(TriggerUploadRequest request)
|
||||
{
|
||||
using var a = _activity.Start("Triggering Processing");
|
||||
try
|
||||
{
|
||||
Uri processUri = new($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/process", UriKind.Relative);
|
||||
string requestBody = JsonConvert.SerializeObject(request);
|
||||
using var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
|
||||
|
||||
using HttpResponseMessage processResponse = await _speckleClient
|
||||
.PostAsync(processUri, content, _cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
string body = await processResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
processResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
a?.SetStatus(SdkActivityStatusCode.Error);
|
||||
a?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_speckleClient.Dispose();
|
||||
_s3Client.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
|
||||
namespace Speckle.Sdk.Pipelines.Send;
|
||||
|
||||
public sealed record UploadItem(string Id, EfficientJson Json, string SpeckleType, ObjectReference Reference)
|
||||
: IDisposable
|
||||
{
|
||||
public void Dispose() => Json.Dispose();
|
||||
}
|
||||
|
||||
internal record PresignedUploadResponse
|
||||
{
|
||||
public required Uri Url { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public Dictionary<string, string> AdditionalRequestHeaders { get; init; } = new();
|
||||
}
|
||||
|
||||
internal readonly struct TriggerUploadRequest
|
||||
{
|
||||
[JsonProperty("etag")]
|
||||
public required string Etag { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Security.Cryptography;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
@@ -16,4 +17,20 @@ public static class IdGenerator
|
||||
#endif
|
||||
return new Id(hash);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string ComputeId(ReadOnlySpan<byte> input)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(input, hash);
|
||||
|
||||
Span<char> output = stackalloc char[32];
|
||||
|
||||
for (int i = 0, j = 0; j < 32; i += sizeof(byte), j += sizeof(char))
|
||||
{
|
||||
hash[i].TryFormat(output[j..], out _, "x2");
|
||||
}
|
||||
|
||||
return new string(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace Speckle.Sdk.Serialisation;
|
||||
using System.Buffers;
|
||||
using Speckle.Sdk.Dependencies;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation;
|
||||
|
||||
public readonly record struct SerializationResult(Json Json, Id? Id);
|
||||
|
||||
@@ -14,6 +17,26 @@ public readonly record struct Json
|
||||
public string Value { get; }
|
||||
}
|
||||
|
||||
public sealed class EfficientJson : IDisposable
|
||||
{
|
||||
private readonly ArrayBufferWriter<byte> _value;
|
||||
|
||||
public EfficientJson()
|
||||
{
|
||||
_value = Pools.ArrayBufferWriter.Get();
|
||||
}
|
||||
|
||||
internal IBufferWriter<byte> Buffer => _value;
|
||||
|
||||
public ReadOnlySpan<byte> WrittenSpan => _value.WrittenSpan;
|
||||
|
||||
public ReadOnlyMemory<byte> WrittenMemory => _value.WrittenMemory;
|
||||
|
||||
public int WrittenCount => _value.WrittenCount;
|
||||
|
||||
public void Dispose() => Pools.ArrayBufferWriter.Return(_value);
|
||||
}
|
||||
|
||||
public readonly record struct Id
|
||||
{
|
||||
public Id(string id)
|
||||
|
||||
@@ -2,7 +2,13 @@ using System.Text;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation.V2.Send;
|
||||
|
||||
public sealed record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>? Closures) : IHasByteSize
|
||||
public sealed record BaseItem(
|
||||
Id Id,
|
||||
Json Json,
|
||||
bool NeedsStorage,
|
||||
Dictionary<Id, int>? Closures,
|
||||
bool? IsReference = false
|
||||
) : IHasByteSize
|
||||
{
|
||||
public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value);
|
||||
|
||||
|
||||
@@ -113,16 +113,6 @@ public sealed class SerializeProcess(
|
||||
_processSource.Token
|
||||
);
|
||||
var findTotalObjectsTask = Task.CompletedTask;
|
||||
if (!options.SkipFindTotalObjects)
|
||||
{
|
||||
ThrowIfFailed();
|
||||
findTotalObjectsTask = Task.Factory.StartNew(
|
||||
() => TraverseTotal(root),
|
||||
_processSource.Token,
|
||||
TaskCreationOptions.AttachedToParent | TaskCreationOptions.PreferFairness,
|
||||
_highest
|
||||
);
|
||||
}
|
||||
|
||||
await Traverse(root).ConfigureAwait(false);
|
||||
ThrowIfFailed();
|
||||
@@ -133,6 +123,7 @@ public sealed class SerializeProcess(
|
||||
ThrowIfFailed();
|
||||
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
||||
ThrowIfFailed();
|
||||
|
||||
return new(root.id.NotNull(), baseSerializer.ObjectReferences.Freeze());
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Speckle.Sdk.Dependencies;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Pipelines.Progress;
|
||||
using Speckle.Sdk.Serialisation.V2;
|
||||
using Speckle.Sdk.Serialisation.V2.Receive;
|
||||
using Speckle.Sdk.Serialisation.V2.Send;
|
||||
@@ -96,7 +97,8 @@ public static class ServiceRegistration
|
||||
typeof(DeserializeProcess),
|
||||
typeof(ObjectLoader),
|
||||
typeof(TraversalRule),
|
||||
typeof(Client)
|
||||
typeof(Client),
|
||||
typeof(IngestionProgressManager)
|
||||
);
|
||||
serviceCollection.AddMatchingInterfacesAsTransient(typeof(GraphQLRetry).Assembly);
|
||||
return serviceCollection;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<Configurations>Debug;Release;Local</Configurations>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
@@ -35,7 +35,6 @@
|
||||
<PackageReference Include="Microsoft.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Speckle.Sdk.Dependencies\Speckle.Sdk.Dependencies.csproj" />
|
||||
|
||||
@@ -1,299 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"GraphQL.Client": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0",
|
||||
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.7.0, )",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.0.5, )",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "KGxbPeWsQMnmQy43DSBxAFtHz3l2JX8EWBSGUCvT3CuZ8KsuzbkqMIJMDOxWtG8eZSoCDI04aiVQjWuuV8HmSw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "7.0.5",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "2.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.DoubleNumerics": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "20DtS+FsDRsOD9+AU3TwNFZ0qrKo5f6f7B5ZR9wStsIHHHC9k7DpjbCvuNtmnSjx54MD+TJC7wV2f5iyGVPj1A=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"Speckle.Newtonsoft.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[13.0.2, )",
|
||||
"resolved": "13.0.2",
|
||||
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
|
||||
},
|
||||
"GraphQL.Client.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions.Websocket": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "FTerRmQPqHrCrnoUzhBu+E+1DNGwyrAMLqHkAqOOOu5pGfyMOj8qQUBxI/gDtWtG11p49UxSfWmBzRNlwZqfUg==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.ComponentModel.Annotations": "4.5.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw=="
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.4.0",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.2"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
|
||||
"dependencies": {
|
||||
"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
"contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw=="
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"speckle.sdk.dependencies": {
|
||||
"type": "Project"
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"GraphQL.Client": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
||||
},
|
||||
|
||||
@@ -184,7 +184,7 @@ public class SerializationTests
|
||||
idToBase.Count.Should().Be(count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Theory(Skip = "Takes too long")]
|
||||
[InlineData(1)]
|
||||
[InlineData(4)]
|
||||
public async Task Roundtrip_Test_New(int concurrency)
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ServerObjectManagerTests : MoqTest
|
||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||
|
||||
var activityFactory = Create<ISdkActivityFactory>();
|
||||
activityFactory.Setup(x => x.Start(null, "DownloadObjects")).Returns((ISdkActivity?)null);
|
||||
activityFactory.Setup(x => x.Start(null, default, "DownloadObjects")).Returns((ISdkActivity?)null);
|
||||
|
||||
var serverObjectManager = new ServerObjectManager(
|
||||
http.Object,
|
||||
@@ -91,7 +91,7 @@ public class ServerObjectManagerTests : MoqTest
|
||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||
|
||||
var activityFactory = Create<ISdkActivityFactory>();
|
||||
activityFactory.Setup(x => x.Start(null, "DownloadSingleObject")).Returns((ISdkActivity?)null);
|
||||
activityFactory.Setup(x => x.Start(null, default, "DownloadSingleObject")).Returns((ISdkActivity?)null);
|
||||
|
||||
var serverObjectManager = new ServerObjectManager(
|
||||
http.Object,
|
||||
@@ -132,7 +132,7 @@ public class ServerObjectManagerTests : MoqTest
|
||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||
|
||||
var activityFactory = Create<ISdkActivityFactory>();
|
||||
activityFactory.Setup(x => x.Start(null, "HasObjects")).Returns((ISdkActivity?)null);
|
||||
activityFactory.Setup(x => x.Start(null, default, "HasObjects")).Returns((ISdkActivity?)null);
|
||||
|
||||
var serverObjectManager = new ServerObjectManager(
|
||||
http.Object,
|
||||
@@ -171,7 +171,7 @@ public class ServerObjectManagerTests : MoqTest
|
||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||
|
||||
var activityFactory = Create<ISdkActivityFactory>();
|
||||
activityFactory.Setup(x => x.Start(null, "UploadObjects")).Returns((ISdkActivity?)null);
|
||||
activityFactory.Setup(x => x.Start(null, default, "UploadObjects")).Returns((ISdkActivity?)null);
|
||||
|
||||
var serverObjectManager = new ServerObjectManager(
|
||||
http.Object,
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Type": "AggregateException",
|
||||
"InnerException": {
|
||||
"Data": {},
|
||||
"Message": "NOT_FOUND_ERROR: Model ingestion not found",
|
||||
"Type": "SpeckleGraphQLException"
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Type": "AggregateException",
|
||||
"InnerException": {
|
||||
"Data": {},
|
||||
"Message": "STREAM_NOT_FOUND: Project not found",
|
||||
"Type": "SpeckleGraphQLStreamNotFoundException"
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Type": "AggregateException",
|
||||
"InnerException": {
|
||||
"Data": {},
|
||||
"Message": "NOT_FOUND_ERROR: Model ingestion not found",
|
||||
"Type": "SpeckleGraphQLException"
|
||||
}
|
||||
}
|
||||
+6
-9
@@ -1,10 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
|
||||
@@ -20,9 +17,6 @@ public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
|
||||
@@ -42,7 +36,8 @@ public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
_ = await Sut.Create(createInput);
|
||||
});
|
||||
await Verify(ex);
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLStreamNotFoundException>(item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -54,7 +49,8 @@ public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
_ = await Sut.UpdateProgress(updateInput);
|
||||
});
|
||||
await Verify(ex);
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -69,6 +65,7 @@ public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
_ = await Sut.FailWithCancel(input);
|
||||
});
|
||||
await Verify(ex);
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,11 +1,9 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Transports;
|
||||
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
@@ -25,8 +23,6 @@ public sealed class ModelIngestionResourceTests : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
|
||||
@@ -133,8 +129,10 @@ public sealed class ModelIngestionResourceTests : IAsyncLifetime
|
||||
ModelIngestionSuccessInput finish = new(ingest.id, _project.id, sendResult.RootId, "yay!");
|
||||
string versionId = await Sut.Complete(finish);
|
||||
Version version = await _testUser.Version.Get(versionId, _project.id);
|
||||
ModelIngestion finalIngestion = await _testUser.Ingestion.Get(ingest.id, _project.id);
|
||||
Assert.Equal(version.id, versionId);
|
||||
Assert.Equal(sendResult.RootId, version.referencedObject);
|
||||
Assert.Equal(finalIngestion.statusData.versionId, versionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -151,6 +149,11 @@ public sealed class ModelIngestionResourceTests : IAsyncLifetime
|
||||
ModelIngestion res = await Sut.Get(ingest.id, _project.id);
|
||||
Assert.Equal(ingest.id, res.id);
|
||||
Assert.Equal(ingest.statusData.status, res.statusData.status);
|
||||
Assert.Equal(ingest.statusData.versionId, res.statusData.versionId);
|
||||
Assert.Null(res.statusData.versionId);
|
||||
Assert.Equal(_model.id, res.modelId);
|
||||
Assert.Equal(_project.id, res.projectId);
|
||||
Assert.Equal(_testUser.Account.userInfo.id, res.userId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentAssertions;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
@@ -17,7 +18,7 @@ public class ModelResourceTests : IAsyncLifetime
|
||||
{
|
||||
// Runs instead of [SetUp] in NUnit
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||
_project = await _testUser.Project.Create(new("Test project", "", ProjectVisibility.Public));
|
||||
_model = await _testUser.Model.Create(new("Test Model", "", _project.id));
|
||||
}
|
||||
|
||||
@@ -123,4 +124,40 @@ public class ModelResourceTests : IAsyncLifetime
|
||||
var delEx = await FluentActions.Invoking(() => Sut.Delete(input)).Should().ThrowAsync<AggregateException>();
|
||||
getEx.WithInnerExceptionExactly<SpeckleGraphQLException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestUserHasModelPermissions()
|
||||
{
|
||||
var ownerResult = await Sut.GetPermissions(_project.id, _model.id);
|
||||
ownerResult.canUpdate.authorized.Should().Be(true);
|
||||
ownerResult.canCreateVersion.authorized.Should().Be(true);
|
||||
ownerResult.canDelete.authorized.Should().Be(true);
|
||||
|
||||
// Test with another user
|
||||
var guest = await Fixtures.SeedUserWithClient();
|
||||
|
||||
var guestResult = await guest.Model.GetPermissions(_project.id, _model.id);
|
||||
guestResult.canUpdate.authorized.Should().Be(false);
|
||||
guestResult.canCreateVersion.authorized.Should().Be(false);
|
||||
guestResult.canDelete.authorized.Should().Be(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Server", "Internal")]
|
||||
public async Task TestCanCreateModelIngestion_InternalServer()
|
||||
{
|
||||
var ownerResult = await Sut.CanCreateModelIngestion(_project.id, _model.id);
|
||||
ownerResult.authorized.Should().Be(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Server", "Public")]
|
||||
public async Task TestCanCreateModelIngestion_PublicServer_Throws()
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
await Sut.CanCreateModelIngestion(_project.id, _model.id)
|
||||
);
|
||||
ex.InnerExceptions.Should().HaveCount(1);
|
||||
ex.InnerExceptions.Should().AllBeOfType<SpeckleGraphQLInvalidQueryException>();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ public class ProjectResourceExceptionalTests : IAsyncLifetime
|
||||
ProjectUpdateRoleInput input = new(_secondUser.Account.id.NotNull(), "NonExistentProject", newRole);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.UpdateRole(input));
|
||||
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLForbiddenException>();
|
||||
ex.InnerExceptions.Single().Should().BeAssignableTo<SpeckleGraphQLException>(); //v3 server responds with SpeckleGraphQLStreamNotFoundException exception, v2 reponds with SpeckleGraphQLForbiddenException
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -103,8 +103,30 @@ public class ProjectResourceTests
|
||||
[Fact]
|
||||
public async Task TestUserHasProjectPermissions()
|
||||
{
|
||||
var res = await Sut.GetPermissions(_testProject.id);
|
||||
var privateProject = await _testUser.Project.Create(
|
||||
new ProjectCreateInput("asdfasdf", "desc", ProjectVisibility.Private)
|
||||
);
|
||||
|
||||
var resp = await Sut.GetPermissions(privateProject.id);
|
||||
resp.canCreateModel.authorized.Should().Be(true);
|
||||
resp.canDelete.authorized.Should().Be(true);
|
||||
resp.canLoad.authorized.Should().Be(true);
|
||||
|
||||
var publicProject = await _testUser.Project.Create(
|
||||
new ProjectCreateInput("asdfasdf", "desc", ProjectVisibility.Public)
|
||||
);
|
||||
|
||||
var res = await Sut.GetPermissions(publicProject.id);
|
||||
res.canCreateModel.authorized.Should().Be(true);
|
||||
res.canDelete.authorized.Should().Be(true);
|
||||
res.canLoad.authorized.Should().Be(true);
|
||||
|
||||
// Test with another user
|
||||
var guest = await Fixtures.SeedUserWithClient();
|
||||
|
||||
var guestResult = await guest.Project.GetPermissions(publicProject.id);
|
||||
guestResult.canCreateModel.authorized.Should().Be(false);
|
||||
guestResult.canDelete.authorized.Should().Be(false);
|
||||
guestResult.canLoad.authorized.Should().Be(false);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -11,11 +11,11 @@ namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
public class SubscriptionResourceTests : IAsyncLifetime
|
||||
{
|
||||
#if DEBUG
|
||||
private const int WAIT_PERIOD = 4000; // WSL is slow AF, so for local runs, we're being extra generous
|
||||
private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
|
||||
#else
|
||||
private const int WAIT_PERIOD = 400; // For CI runs, a much smaller wait time is acceptable
|
||||
private const int WAIT_PERIOD = 600; // For CI runs, a much smaller wait time is acceptable
|
||||
#endif
|
||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 400;
|
||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 600;
|
||||
private IClient _testUser;
|
||||
private Project _testProject;
|
||||
private Model _testModel;
|
||||
@@ -32,6 +32,7 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
await _testUser.InitializeWebsocket();
|
||||
_testProject = await _testUser.Project.Create(new("test project123", "desc", null));
|
||||
_testModel = await _testUser.Model.Create(new("test model", "desc", _testProject.id));
|
||||
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Type": "AggregateException",
|
||||
"InnerException": {
|
||||
"Data": {},
|
||||
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
|
||||
"Type": "SpeckleGraphQLForbiddenException"
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Type": "AggregateException",
|
||||
"InnerException": {
|
||||
"Data": {},
|
||||
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
|
||||
"Type": "SpeckleGraphQLForbiddenException"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Speckle.Sdk.Api;
|
||||
using FluentAssertions;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
@@ -25,13 +26,15 @@ public class WorkspaceResourceTests
|
||||
public async Task TestGetWorkspace()
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
|
||||
await Verify(ex);
|
||||
ex.InnerExceptions.Should().HaveCount(1);
|
||||
ex.InnerExceptions.Should().AllBeOfType<SpeckleGraphQLForbiddenException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestGetProjects()
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
|
||||
await Verify(ex);
|
||||
ex.InnerExceptions.Should().HaveCount(1);
|
||||
ex.InnerExceptions.Should().AllBeOfType<SpeckleGraphQLForbiddenException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Sdk.Tests.Integration;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresSqLiteAccountDb), DisableParallelization = true)]
|
||||
public sealed class RequiresSqLiteAccountDb;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresAuthFlowPort), DisableParallelization = true)]
|
||||
public sealed class RequiresAuthFlowPort;
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public class AccountManagerTests
|
||||
{
|
||||
private IAccountManager _sut;
|
||||
|
||||
public AccountManagerTests()
|
||||
{
|
||||
_sut = Fixtures.ServiceProvider.GetRequiredService<IAccountManager>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAccount_UpdatesUserInfo()
|
||||
{
|
||||
using IClient user = await Fixtures.SeedUserWithClient();
|
||||
string realAccountId = user.Account.id;
|
||||
UserInfo realUserData = user.Account.userInfo;
|
||||
UserInfo staleData = new()
|
||||
{
|
||||
avatar = "my old avatar",
|
||||
company = "my old company",
|
||||
email = "my.old.email@example.com",
|
||||
id = realUserData.id,
|
||||
name = "my old name",
|
||||
};
|
||||
// Mutate with "fake" data to simulate a stale account data
|
||||
user.Account.userInfo = staleData;
|
||||
user.Account.id = null!; //force re-generate id
|
||||
|
||||
Assert.NotEqual(realAccountId, user.Account.id);
|
||||
|
||||
await _sut.UpdateAccountInMemory(user.Account);
|
||||
|
||||
Assert.Equal(realUserData.avatar, user.Account.userInfo.avatar);
|
||||
Assert.Equal(realUserData.company, user.Account.userInfo.company);
|
||||
Assert.Equal(realUserData.email, user.Account.userInfo.email);
|
||||
Assert.Equal(realUserData.id, user.Account.userInfo.id);
|
||||
Assert.Equal(realUserData.name, user.Account.userInfo.name);
|
||||
Assert.Equal(realAccountId, user.Account.id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAccount_UpdatesServerInfo()
|
||||
{
|
||||
using IClient user = await Fixtures.SeedUserWithClient();
|
||||
string realAccountId = user.Account.id;
|
||||
ServerInfo realServerData = user.Account.serverInfo;
|
||||
ServerInfo staleData = new()
|
||||
{
|
||||
company = "This old company",
|
||||
description = "this old description",
|
||||
name = "This old name",
|
||||
url = realServerData.url,
|
||||
version = "0.0.123",
|
||||
};
|
||||
// Mutate with "fake" data to simulate a stale account data
|
||||
user.Account.serverInfo = staleData;
|
||||
user.Account.id = null!; //force re-generate id
|
||||
|
||||
Assert.Equal(realAccountId, user.Account.id); //account id should not change since we didn't change server url
|
||||
|
||||
await _sut.UpdateAccountInMemory(user.Account);
|
||||
|
||||
Assert.Equal(realServerData.company, user.Account.serverInfo.company);
|
||||
Assert.Equal(realServerData.description, user.Account.serverInfo.description);
|
||||
Assert.Equal(realServerData.name, user.Account.serverInfo.name);
|
||||
Assert.Equal(realServerData.url, user.Account.serverInfo.url);
|
||||
Assert.Equal(realServerData.version, user.Account.serverInfo.version);
|
||||
Assert.Equal(realAccountId, user.Account.id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAccount_ServerInfoMigration()
|
||||
{
|
||||
using IClient user = await Fixtures.SeedUserWithClient();
|
||||
string realAccountId = user.Account.id;
|
||||
ServerInfo realServerData = user.Account.serverInfo;
|
||||
ServerInfo staleData = new()
|
||||
{
|
||||
company = "This old company",
|
||||
description = "this old description",
|
||||
name = "This old name",
|
||||
url = realServerData.url,
|
||||
version = "0.0.123",
|
||||
};
|
||||
// Mutate with "fake" data to simulate a stale account data
|
||||
user.Account.serverInfo = staleData;
|
||||
user.Account.id = null!; //force re-generate id
|
||||
|
||||
Assert.Equal(realAccountId, user.Account.id); //account id should not change since we didn't change server url
|
||||
|
||||
await _sut.UpdateAccountInMemory(user.Account);
|
||||
|
||||
Assert.Equal(realServerData.company, user.Account.serverInfo.company);
|
||||
Assert.Equal(realServerData.description, user.Account.serverInfo.description);
|
||||
Assert.Equal(realServerData.name, user.Account.serverInfo.name);
|
||||
Assert.Equal(realServerData.url, user.Account.serverInfo.url);
|
||||
Assert.Equal(realServerData.version, user.Account.serverInfo.version);
|
||||
Assert.Equal(realAccountId, user.Account.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresAuthFlowPort))]
|
||||
public class AuthFlowExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
private IAuthFlow _authFlow;
|
||||
private IClient _client;
|
||||
private readonly Uri _url = AuthApp.ConnectorsV3.CallbackUrl;
|
||||
|
||||
[Fact]
|
||||
public async Task GetRefreshToken_Cancellation()
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
|
||||
_ = await _authFlow.GetRefreshedToken(
|
||||
_client.Account.refreshToken,
|
||||
_client.ServerUrl,
|
||||
Fixtures.TestAuthApp,
|
||||
new(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRefreshToken_UnknownApp()
|
||||
{
|
||||
//interestingly, the server responds with a 401 Unauthorized despite internally being a bad request
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
_ = await _authFlow.GetRefreshedToken(
|
||||
_client.Account.refreshToken,
|
||||
_client.ServerUrl,
|
||||
new()
|
||||
{
|
||||
AppId = "doesn't exist",
|
||||
AppSecret = "doesn't exist",
|
||||
CallbackUrl = new("invalid://localhost"),
|
||||
},
|
||||
CancellationToken.None
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRefreshToken_NullRefreshToken()
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
_ = await _authFlow.GetRefreshedToken(null, _client.ServerUrl, AuthApp.ConnectorsV3, CancellationToken.None)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SimultaneousListeners_SamePort_OneFails()
|
||||
{
|
||||
using CancellationTokenSource ct = new();
|
||||
var task1 = AuthFlow.RunListener(_url, ct.Token);
|
||||
await Task.Delay(50, CancellationToken.None);
|
||||
|
||||
await Assert.ThrowsAsync<HttpListenerException>(async () => await AuthFlow.RunListener(_url, ct.Token));
|
||||
|
||||
if (task1.IsCompleted)
|
||||
{
|
||||
throw new InvalidOperationException("Was expecting task to still be running", task1.Exception);
|
||||
}
|
||||
|
||||
await ct.CancelAsync();
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task1);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_authFlow = Fixtures.ServiceProvider.GetRequiredService<IAuthFlow>();
|
||||
_client = await Fixtures.SeedUserWithClient();
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresAuthFlowPort))]
|
||||
public sealed class AuthFlowTests
|
||||
{
|
||||
private readonly IAuthFlow _authFlow;
|
||||
private readonly Uri _url = AuthApp.ConnectorsV3.CallbackUrl;
|
||||
|
||||
public AuthFlowTests()
|
||||
{
|
||||
_authFlow = Fixtures.ServiceProvider.GetRequiredService<IAuthFlow>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunListener_ReturnsAccessCode_WhenQueryContainsAccessCode()
|
||||
{
|
||||
var listenerTask = AuthFlow.RunListener(_url, CancellationToken.None);
|
||||
using var client = new HttpClient();
|
||||
const string EXPECTED_ACCESS_CODE = "abcdef123456";
|
||||
|
||||
var response = await client.GetAsync(new Uri(_url, $"?access_code={EXPECTED_ACCESS_CODE}"));
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string result = await listenerTask;
|
||||
|
||||
Assert.Equal(EXPECTED_ACCESS_CODE, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunListener_Throws_InvalidAccessCode()
|
||||
{
|
||||
var listenerTask = AuthFlow.RunListener(_url, CancellationToken.None);
|
||||
using var client = new HttpClient();
|
||||
|
||||
var response = await client.GetAsync(new Uri(_url, ""));
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await Assert.ThrowsAsync<AuthFlowException>(async () =>
|
||||
{
|
||||
_ = await listenerTask;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunListener_Throws_Cancellation()
|
||||
{
|
||||
using CancellationTokenSource cancellationTokenSource = new();
|
||||
var listenerTask = AuthFlow.RunListener(_url, cancellationTokenSource.Token);
|
||||
|
||||
await cancellationTokenSource.CancelAsync();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
_ = await listenerTask;
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.1)]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
public async Task RunListener_Timeout(double timeS)
|
||||
{
|
||||
await Assert.ThrowsAsync<TimeoutException>(async () =>
|
||||
{
|
||||
_ = await _authFlow.RunListenerWithTimeout(_url, TimeSpan.FromSeconds(timeS), CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanGetRefreshToken()
|
||||
{
|
||||
using var user = await Fixtures.SeedUserWithClient();
|
||||
var tokenExchange = await _authFlow.GetRefreshedToken(
|
||||
user.Account.refreshToken,
|
||||
user.ServerUrl,
|
||||
Fixtures.TestAuthApp,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.NotNull(tokenExchange.token);
|
||||
Assert.NotNull(tokenExchange.refreshToken);
|
||||
|
||||
user.Account.token = tokenExchange.token;
|
||||
user.Account.refreshToken = tokenExchange.refreshToken;
|
||||
|
||||
var apiTest = await user.ActiveUser.Get();
|
||||
|
||||
Assert.NotNull(apiTest);
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,22 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
[assembly: AssemblyTrait("Category", "Integration")]
|
||||
|
||||
#if DEBUG
|
||||
[assembly: CollectionBehavior(MaxParallelThreads = 8)]
|
||||
|
||||
#endif
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration;
|
||||
|
||||
public static class Fixtures
|
||||
{
|
||||
public static readonly ServerInfo Server = new() { url = "http://localhost:3000", name = "Docker Server" };
|
||||
|
||||
public static readonly AuthApp TestAuthApp = new()
|
||||
{
|
||||
AppId = "spklwebapp",
|
||||
AppSecret = "spklwebapp",
|
||||
CallbackUrl = new Uri("invaid://localhost"),
|
||||
};
|
||||
public static IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
static Fixtures()
|
||||
@@ -90,8 +100,8 @@ public static class Fixtures
|
||||
Dictionary<string, string> tokenBody = new()
|
||||
{
|
||||
["accessCode"] = accessCode,
|
||||
["appId"] = "spklwebapp",
|
||||
["appSecret"] = "spklwebapp",
|
||||
["appId"] = TestAuthApp.AppId,
|
||||
["appSecret"] = TestAuthApp.AppSecret,
|
||||
["challenge"] = "challengingchallenge",
|
||||
};
|
||||
|
||||
@@ -104,8 +114,11 @@ public static class Fixtures
|
||||
);
|
||||
|
||||
var token = deserialised.NotNull()["token"].NotNull();
|
||||
var refreshToken = deserialised.NotNull()["refreshToken"].NotNull();
|
||||
|
||||
return await ServiceProvider.GetRequiredService<IAccountFactory>().CreateAccount(new(Server.url), token);
|
||||
return await ServiceProvider
|
||||
.GetRequiredService<IAccountFactory>()
|
||||
.CreateAccount(new(Server.url), token, refreshToken);
|
||||
}
|
||||
|
||||
public static Base GenerateSimpleObject()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
@@ -16,8 +14,7 @@ public class MemoryTransportTests : IDisposable
|
||||
public MemoryTransportTests()
|
||||
{
|
||||
CleanData();
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Pipelines.Progress;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.Pipelines.Progress;
|
||||
|
||||
[Trait("Server", "Internal")]
|
||||
public class IngestionProgressManagerTests : IAsyncLifetime
|
||||
{
|
||||
private IIngestionProgressManagerFactory _factory;
|
||||
private IClient _client;
|
||||
private Project _project;
|
||||
private Model _model;
|
||||
private ModelIngestion _ingestion;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_factory = serviceProvider.GetRequiredService<IIngestionProgressManagerFactory>();
|
||||
|
||||
_client = await Fixtures.SeedUserWithClient();
|
||||
_project = await _client.Project.Create(new("test", null, default));
|
||||
_model = await _client.Model.Create(new("test", null, _project.id));
|
||||
_ingestion = await _client.Ingestion.Create(
|
||||
new(_model.id, _project.id, "Testing ingestion", new("integrationTests", "0.0.0", null, null))
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestProgress_NoThrottle()
|
||||
{
|
||||
var sut = _factory.CreateInstance(_client, _ingestion, TimeSpan.Zero, CancellationToken.None);
|
||||
const string FIRST_MESSAGE = "This is a test 123";
|
||||
const string SECOND_MESSAGE = "This is another test 321";
|
||||
|
||||
// first message (should go through)
|
||||
sut.Report(new CardProgress(FIRST_MESSAGE, 0.123123123d));
|
||||
await sut.LastUpdate.NotNull();
|
||||
var res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(FIRST_MESSAGE, res.statusData.progressMessage);
|
||||
|
||||
// second message (should also go through)
|
||||
sut.Report(new CardProgress(SECOND_MESSAGE, 0.321321321d));
|
||||
await sut.LastUpdate.NotNull();
|
||||
res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(SECOND_MESSAGE, res.statusData.progressMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestProgress_WithThrottle()
|
||||
{
|
||||
var sut = _factory.CreateInstance(_client, _ingestion, TimeSpan.FromMilliseconds(500), CancellationToken.None);
|
||||
const string EXPECTED_MESSAGE = "First message should go through 123";
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(600));
|
||||
|
||||
// first message (should go through)
|
||||
sut.Report(new CardProgress(EXPECTED_MESSAGE, 0.123123123d));
|
||||
// second message (should be dropped)
|
||||
sut.Report(new CardProgress("Second message, should be dropped", 0.321321321d));
|
||||
await sut.LastUpdate.NotNull();
|
||||
var res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(EXPECTED_MESSAGE, res.statusData.progressMessage);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration;
|
||||
|
||||
@@ -19,8 +16,6 @@ public sealed class SendReceiveTests : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
ClearCache();
|
||||
|
||||
@@ -16,4 +16,7 @@
|
||||
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Pipelines\Send\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -374,7 +374,7 @@
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
||||
},
|
||||
|
||||
@@ -10,14 +10,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class Closures
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public Closures()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public sealed partial class OperationsReceiveTests : IDisposable
|
||||
{
|
||||
private static readonly Base[] s_testObjects;
|
||||
@@ -44,8 +45,7 @@ public sealed partial class OperationsReceiveTests : IDisposable
|
||||
|
||||
private static void Reset()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> TestCases()
|
||||
|
||||
@@ -7,14 +7,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SendObjectReferences
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SendObjectReferences()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public sealed class SendReceiveLocal : IDisposable
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SendReceiveLocal()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ using Point = Speckle.Sdk.Tests.Unit.Host.Point;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class ObjectSerialization
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public ObjectSerialization()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
[assembly: CollectionBehavior()]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Sdk.Tests.Unit;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresTypeLoaderCollection), DisableParallelization = true)]
|
||||
public class RequiresTypeLoaderCollection;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresSqLiteAccountDb), DisableParallelization = true)]
|
||||
public class RequiresSqLiteAccountDb;
|
||||
@@ -3,12 +3,12 @@ using Moq;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.SQLite;
|
||||
using Speckle.Sdk.Testing;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public sealed class AccountManagerTests : MoqTest
|
||||
{
|
||||
private class TestAccountFactory : IAccountFactory
|
||||
@@ -27,14 +27,11 @@ public sealed class AccountManagerTests : MoqTest
|
||||
) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private readonly Mock<ISpeckleApplication> _mockApplication;
|
||||
private readonly Mock<ILogger<AccountManager>> _mockLogger;
|
||||
private readonly Mock<IGraphQLClientFactory> _mockGraphQLClientFactory;
|
||||
private readonly Mock<ISpeckleHttp> _mockSpeckleHttp;
|
||||
private readonly IAccountFactory _mockAccountFactory;
|
||||
private readonly Mock<ISqLiteJsonCacheManagerFactory> _mockSqLiteJsonCacheManagerFactory;
|
||||
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountStorage;
|
||||
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountAddLockStorage;
|
||||
private readonly Mock<IAuthFlow> _mockAuthFlow;
|
||||
|
||||
#pragma warning disable CA2213
|
||||
private readonly AccountManager _accountManager;
|
||||
@@ -42,27 +39,19 @@ public sealed class AccountManagerTests : MoqTest
|
||||
|
||||
public AccountManagerTests()
|
||||
{
|
||||
_mockApplication = Create<ISpeckleApplication>();
|
||||
_mockLogger = Create<ILogger<AccountManager>>(MockBehavior.Loose);
|
||||
_mockGraphQLClientFactory = Create<IGraphQLClientFactory>();
|
||||
_mockSpeckleHttp = Create<ISpeckleHttp>();
|
||||
_mockAccountFactory = new TestAccountFactory();
|
||||
_mockSqLiteJsonCacheManagerFactory = Create<ISqLiteJsonCacheManagerFactory>();
|
||||
_mockAuthFlow = Create<IAuthFlow>();
|
||||
|
||||
_mockAccountStorage = Create<ISqLiteJsonCacheManager>();
|
||||
_mockAccountAddLockStorage = Create<ISqLiteJsonCacheManager>();
|
||||
|
||||
_mockSqLiteJsonCacheManagerFactory.Setup(f => f.CreateForUser("Accounts")).Returns(_mockAccountStorage.Object);
|
||||
_mockSqLiteJsonCacheManagerFactory
|
||||
.Setup(f => f.CreateForUser("AccountAddFlow"))
|
||||
.Returns(_mockAccountAddLockStorage.Object);
|
||||
|
||||
_accountManager = new AccountManager(
|
||||
_mockApplication.Object,
|
||||
_mockLogger.Object,
|
||||
_mockGraphQLClientFactory.Object,
|
||||
_mockSpeckleHttp.Object,
|
||||
_mockAccountFactory,
|
||||
_mockAuthFlow.Object,
|
||||
_mockSqLiteJsonCacheManagerFactory.Object
|
||||
);
|
||||
}
|
||||
@@ -329,71 +318,6 @@ public sealed class AccountManagerTests : MoqTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocalIdentifierForAccount_ReturnsIdentifier_WhenAccountExists()
|
||||
{
|
||||
// Arrange
|
||||
var account = CreateTestAccount("test-account");
|
||||
var expectedUri = new Uri($"{account.serverInfo.url}?id={account.userInfo.id}");
|
||||
|
||||
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
|
||||
|
||||
// Act
|
||||
var result = _accountManager.GetLocalIdentifierForAccount(account);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(expectedUri, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLocalIdentifierForAccount_ReturnsNull_WhenAccountDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var account = CreateTestAccount("non-existent-account");
|
||||
|
||||
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns([]);
|
||||
|
||||
// Act
|
||||
var result = _accountManager.GetLocalIdentifierForAccount(account);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAccountForLocalIdentifier_ReturnsAccount_WhenMatches()
|
||||
{
|
||||
// Arrange
|
||||
var account = CreateTestAccount("test-account");
|
||||
var localIdentifier = new Uri($"{account.serverInfo.url}?id={account.userInfo.id}");
|
||||
|
||||
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
|
||||
|
||||
// Act
|
||||
var result = _accountManager.GetAccountForLocalIdentifier(localIdentifier);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(account.id, result!.id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAccountForLocalIdentifier_ReturnsNull_WhenNoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var account = CreateTestAccount("test-account");
|
||||
var localIdentifier = new Uri("https://different.url?u=different-user");
|
||||
|
||||
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
|
||||
|
||||
// Act
|
||||
var result = _accountManager.GetAccountForLocalIdentifier(localIdentifier);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// Helper method to create a test account
|
||||
private static Account CreateTestAccount(string id)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public class AccountServerMigrationTests : IDisposable
|
||||
{
|
||||
private readonly List<Account> _accountsToCleanUp = [];
|
||||
|
||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public class CredentialInfrastructure : IDisposable
|
||||
{
|
||||
private readonly IAccountManager _accountManager;
|
||||
@@ -47,7 +48,7 @@ public class CredentialInfrastructure : IDisposable
|
||||
{
|
||||
Fixtures.UpdateOrSaveAccount(s_testAccount1);
|
||||
Fixtures.UpdateOrSaveAccount(s_testAccount2);
|
||||
Fixtures.SaveLocalAccount(s_testAccount3);
|
||||
Fixtures.UpdateOrSaveAccount(s_testAccount3);
|
||||
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_accountManager = serviceProvider.GetRequiredService<IAccountManager>();
|
||||
@@ -59,7 +60,6 @@ public class CredentialInfrastructure : IDisposable
|
||||
Fixtures.DeleteLocalAccount(s_testAccount1.id);
|
||||
Fixtures.DeleteLocalAccount(s_testAccount2.id);
|
||||
Fixtures.DeleteLocalAccount(s_testAccount3.id);
|
||||
Fixtures.DeleteLocalAccountFile();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -92,7 +92,7 @@ public class CredentialInfrastructure : IDisposable
|
||||
{
|
||||
var accs = _accountManager.GetAccounts(target.serverInfo.url).ToList();
|
||||
|
||||
accs.Count.Should().Be(1);
|
||||
accs.Should().HaveCount(1);
|
||||
|
||||
var acc = accs[0];
|
||||
|
||||
@@ -102,24 +102,4 @@ public class CredentialInfrastructure : IDisposable
|
||||
acc.refreshToken.Should().Be(target.refreshToken);
|
||||
acc.token.Should().Be(target.token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureLocalIdentifiers_AreUniqueAcrossServers()
|
||||
{
|
||||
// Accounts with the same user ID in different servers should always result in different local identifiers.
|
||||
string id = "12345";
|
||||
var acc1 = new Account
|
||||
{
|
||||
serverInfo = new ServerInfo { url = "https://speckle.xyz" },
|
||||
userInfo = new UserInfo { id = id },
|
||||
}.GetLocalIdentifier();
|
||||
|
||||
var acc2 = new Account
|
||||
{
|
||||
serverInfo = new ServerInfo { url = "https://app.speckle.systems" },
|
||||
userInfo = new UserInfo { id = id },
|
||||
}.GetLocalIdentifier();
|
||||
|
||||
acc1.Should().NotBe(acc2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
public class AuthFlowTests
|
||||
{
|
||||
private const int REPEAT = 20;
|
||||
|
||||
[Fact]
|
||||
public void GenerateChallenge_ReturnsValidUniqueChallenge()
|
||||
{
|
||||
var codeVerifiers = Enumerable.Range(0, REPEAT).Select(_ => AuthFlow.GenerateCodeVerifier()).ToArray();
|
||||
|
||||
Assert.All(
|
||||
codeVerifiers,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal(43, item.Length);
|
||||
Assert.Matches(@"^[A-Za-z0-9\-_+/]*$", item);
|
||||
}
|
||||
);
|
||||
|
||||
Assert.Equivalent(codeVerifiers, codeVerifiers.Distinct());
|
||||
var challenges = codeVerifiers.Select(AuthFlow.GenerateCodeChallenge).ToArray();
|
||||
|
||||
Assert.All(
|
||||
challenges,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal(43, item.Length);
|
||||
Assert.Matches(@"^[A-Za-z0-9\-_+/]*$", item);
|
||||
}
|
||||
);
|
||||
Assert.Equivalent(challenges, challenges.Distinct());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user