Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c517dead03 | |||
| 2b61ab7d2e | |||
| 4b319499c3 | |||
| d4055c6ff1 | |||
| 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 | |||
| 49ef9917c4 | |||
| 94b0473157 | |||
| 8071990dd5 | |||
| 8c7dbc89aa | |||
| 676a3df153 | |||
| c75538e1c7 | |||
| 5d10b77ee4 | |||
| 82dca56fbd |
@@ -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 }}
|
||||||
@@ -6,10 +6,12 @@ on:
|
|||||||
docker-compose-file:
|
docker-compose-file:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
use-github-container-registry:
|
use-internal-image:
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN:
|
||||||
|
required: true
|
||||||
jobs:
|
jobs:
|
||||||
integration-test:
|
integration-test:
|
||||||
env:
|
env:
|
||||||
@@ -27,15 +29,15 @@ jobs:
|
|||||||
cache-dependency-path: "**/packages.lock.json"
|
cache-dependency-path: "**/packages.lock.json"
|
||||||
|
|
||||||
- name: 🔐 Login to Github Container Registry
|
- name: 🔐 Login to Github Container Registry
|
||||||
if: ${{ inputs.use-github-container-registry }}
|
if: ${{ inputs.use-internal-image }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: "ghcr.io"
|
registry: "ghcr.io"
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: ⚙️ Spin up Server
|
- 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
|
- name: 📦 Restore
|
||||||
run: dotnet restore ${{ env.Solution }} --locked-mode
|
run: dotnet restore ${{ env.Solution }} --locked-mode
|
||||||
@@ -43,11 +45,18 @@ jobs:
|
|||||||
- name: 🏗️ Build
|
- name: 🏗️ Build
|
||||||
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
|
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
|
||||||
|
|
||||||
- name: 🔨 Integration Tests
|
- name: 🔨 Integration Tests against Public Server
|
||||||
run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
if: ${{ !inputs.use-internal-image }}
|
||||||
|
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Internal)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||||
|
|
||||||
|
- name: 🔨 Integration Tests against Internal Server
|
||||||
|
if: ${{ inputs.use-internal-image }}
|
||||||
|
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
|
- name: Upload coverage reports to Codecov with GitHub Action
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
fail_ci_if_error: true
|
||||||
files: tests/**/coverage.xml
|
files: tests/**/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
name: PR Test
|
name: PR Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main" # Need to run for codecov to compare against the BASE
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -39,7 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload coverage reports to Codecov with GitHub Action
|
- name: Upload coverage reports to Codecov with GitHub Action
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
fail_ci_if_error: true
|
||||||
files: tests/**/coverage.xml
|
files: tests/**/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
@@ -47,9 +53,13 @@ jobs:
|
|||||||
uses: "./.github/workflows/integration-test.yml"
|
uses: "./.github/workflows/integration-test.yml"
|
||||||
with:
|
with:
|
||||||
docker-compose-file: "docker-compose-internal.yml"
|
docker-compose-file: "docker-compose-internal.yml"
|
||||||
use-github-container-registry: true
|
use-internal-image: true
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
integration-test-public:
|
integration-test-public:
|
||||||
uses: "./.github/workflows/integration-test.yml"
|
uses: "./.github/workflows/integration-test.yml"
|
||||||
with:
|
with:
|
||||||
docker-compose-file: "docker-compose.yml"
|
docker-compose-file: "docker-compose.yml"
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -46,11 +46,6 @@ jobs:
|
|||||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov with GitHub Action
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
files: tests/**/coverage.xml
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
- name: NuGet login (OIDC → temp API key)
|
- name: NuGet login (OIDC → temp API key)
|
||||||
uses: NuGet/login@v1
|
uses: NuGet/login@v1
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dotnet 8.0.400
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[2.2.0,)" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[2.2.0,)" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" 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.Extensions.Logging" Version="[2.2.0,)" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[5.0.0,)" />
|
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
||||||
<PackageVersion Include="Open.ChannelExtensions" Version="9.1.0" />
|
<PackageVersion Include="Open.ChannelExtensions" Version="9.1.0" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<File Path="Directory.Build.props" />
|
<File Path="Directory.Build.props" />
|
||||||
<File Path="Directory.Build.Targets" />
|
<File Path="Directory.Build.Targets" />
|
||||||
<File Path="Directory.Packages.props" />
|
<File Path="Directory.Packages.props" />
|
||||||
|
<File Path="docker-compose-internal.yml" />
|
||||||
<File Path="docker-compose.yml" />
|
<File Path="docker-compose.yml" />
|
||||||
<File Path="global.json" />
|
<File Path="global.json" />
|
||||||
<File Path="README.md" />
|
<File Path="README.md" />
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
<File Path=".github\git-commit-instructions.md" />
|
<File Path=".github\git-commit-instructions.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/config/workflows/">
|
<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/pr.yml" />
|
||||||
<File Path=".github/workflows/release.yml" />
|
<File Path=".github/workflows/release.yml" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
speckle-server:
|
speckle-server:
|
||||||
image: ghcr.io/specklesystems/speckle-server:latest
|
image: ${SPECKLE_SERVER_IMAGE:-ghcr.io/specklesystems/speckle-server:latest}
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -97,10 +97,7 @@ services:
|
|||||||
|
|
||||||
STRATEGY_LOCAL: "true"
|
STRATEGY_LOCAL: "true"
|
||||||
|
|
||||||
POSTGRES_URL: "postgres"
|
POSTGRES_URL: 'postgres://speckle:speckle@postgres:5432/speckle'
|
||||||
POSTGRES_USER: "speckle"
|
|
||||||
POSTGRES_PASSWORD: "speckle"
|
|
||||||
POSTGRES_DB: "speckle"
|
|
||||||
ENABLE_MP: "false"
|
ENABLE_MP: "false"
|
||||||
|
|
||||||
LOG_PRETTY: "true"
|
LOG_PRETTY: "true"
|
||||||
|
|||||||
@@ -281,7 +281,6 @@
|
|||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"GraphQL.Client": "[6.0.0, )",
|
"GraphQL.Client": "[6.0.0, )",
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )",
|
|
||||||
"Microsoft.CSharp": "[4.7.0, )",
|
"Microsoft.CSharp": "[4.7.0, )",
|
||||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||||
@@ -292,7 +291,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speckle.sdk.dependencies": {
|
"speckle.sdk.dependencies": {
|
||||||
"type": "Project"
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": "[9.0.4, )"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"GraphQL.Client": {
|
"GraphQL.Client": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
@@ -307,9 +309,9 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Bcl.AsyncInterfaces": {
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[5.0.0, )",
|
"requested": "[9.0.4, )",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.4",
|
||||||
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
|
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,7 +228,6 @@
|
|||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"GraphQL.Client": "[6.0.0, )",
|
"GraphQL.Client": "[6.0.0, )",
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )",
|
|
||||||
"Microsoft.CSharp": "[4.7.0, )",
|
"Microsoft.CSharp": "[4.7.0, )",
|
||||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||||
@@ -239,7 +238,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speckle.sdk.dependencies": {
|
"speckle.sdk.dependencies": {
|
||||||
"type": "Project"
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": "[9.0.4, )"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"GraphQL.Client": {
|
"GraphQL.Client": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
@@ -254,9 +256,9 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Bcl.AsyncInterfaces": {
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[5.0.0, )",
|
"requested": "[9.0.4, )",
|
||||||
"resolved": "5.0.0",
|
"resolved": "9.0.4",
|
||||||
"contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
|
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public interface ISdkActivity : IDisposable
|
|||||||
void SetTag(string key, object? value);
|
void SetTag(string key, object? value);
|
||||||
void RecordException(Exception e);
|
void RecordException(Exception e);
|
||||||
string TraceId { get; }
|
string TraceId { get; }
|
||||||
|
string SpanId { get; }
|
||||||
void SetStatus(SdkActivityStatusCode code);
|
void SetStatus(SdkActivityStatusCode code);
|
||||||
|
|
||||||
void InjectHeaders(Action<string, string> header);
|
void InjectHeaders(Action<string, string> header);
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using Speckle.Connectors.Logging;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Logging;
|
namespace Speckle.Sdk.Logging;
|
||||||
|
|
||||||
public interface ISdkActivityFactory : IDisposable
|
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 = ""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -28,4 +28,36 @@
|
|||||||
<PackageReference Include="Open.ChannelExtensions" PrivateAssets="all" />
|
<PackageReference Include="Open.ChannelExtensions" PrivateAssets="all" />
|
||||||
<PackageReference Include="System.Threading.Channels" PrivateAssets="all" />
|
<PackageReference Include="System.Threading.Channels" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
|
||||||
|
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||||
|
</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>
|
</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,
|
||||||
|
}
|
||||||
@@ -11,6 +11,15 @@
|
|||||||
"ILRepack": "2.0.33"
|
"ILRepack": "2.0.33"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[9.0.4, )",
|
||||||
|
"resolved": "9.0.4",
|
||||||
|
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.Extensions.ObjectPool": {
|
"Microsoft.Extensions.ObjectPool": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.4, )",
|
"requested": "[9.0.4, )",
|
||||||
@@ -151,15 +160,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
"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": {
|
"net8.0": {
|
||||||
|
|||||||
@@ -193,30 +193,7 @@ public sealed class BlobApi : IBlobApi
|
|||||||
using var response = await _unauthedClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
using var response = await _unauthedClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return ParseEtagHeader(response.Headers);
|
return BlobApiHelpers.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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
|||||||
public WorkspaceResource Workspace { get; }
|
public WorkspaceResource Workspace { get; }
|
||||||
public ServerResource Server { get; }
|
public ServerResource Server { get; }
|
||||||
public FileImportResource FileImport { get; }
|
public FileImportResource FileImport { get; }
|
||||||
|
public ModelIngestionResource Ingestion { get; }
|
||||||
|
|
||||||
public Uri ServerUrl => new(Account.serverInfo.url);
|
public Uri ServerUrl => new(Account.serverInfo.url);
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
|||||||
Workspace = new(this);
|
Workspace = new(this);
|
||||||
Server = new(this);
|
Server = new(this);
|
||||||
FileImport = new(this, blobApiFactory.Create(account));
|
FileImport = new(this, blobApiFactory.Create(account));
|
||||||
|
Ingestion = new(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AutoInterfaceIgnore]
|
[AutoInterfaceIgnore]
|
||||||
@@ -85,6 +87,26 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
|||||||
catch (Exception ex) when (!ex.IsFatal()) { }
|
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) =>
|
internal async Task<T> ExecuteWithResiliencePolicies<T>(Func<Task<T>> func) =>
|
||||||
await GraphQLRetry
|
await GraphQLRetry
|
||||||
.ExecuteAsync<T, SpeckleGraphQLInternalErrorException>(
|
.ExecuteAsync<T, SpeckleGraphQLInternalErrorException>(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
// ReSharper disable InconsistentNaming
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
//This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
/// <remarks>
|
||||||
|
/// This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
||||||
|
/// </remarks>
|
||||||
public enum FileUploadConversionStatus
|
public enum FileUploadConversionStatus
|
||||||
{
|
{
|
||||||
Queued = 0,
|
Queued = 0,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
|
public enum ModelIngestionStatus
|
||||||
|
{
|
||||||
|
cancelled,
|
||||||
|
failed,
|
||||||
|
processing,
|
||||||
|
queued,
|
||||||
|
success,
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectCommentsUpdatedMessageType
|
public enum ProjectCommentsUpdatedMessageType
|
||||||
{
|
{
|
||||||
ARCHIVED,
|
ARCHIVED,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectFileImportUpdatedMessageType
|
public enum ProjectFileImportUpdatedMessageType
|
||||||
{
|
{
|
||||||
CREATED,
|
CREATED,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
|
public enum ProjectModelIngestionUpdatedMessageType
|
||||||
|
{
|
||||||
|
cancellationRequested,
|
||||||
|
created,
|
||||||
|
deleted,
|
||||||
|
updated,
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectModelsUpdatedMessageType
|
public enum ProjectModelsUpdatedMessageType
|
||||||
{
|
{
|
||||||
CREATED,
|
CREATED,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectPendingModelsUpdatedMessageType
|
public enum ProjectPendingModelsUpdatedMessageType
|
||||||
{
|
{
|
||||||
CREATED,
|
CREATED,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectUpdatedMessageType
|
public enum ProjectUpdatedMessageType
|
||||||
{
|
{
|
||||||
DELETED,
|
DELETED,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectVersionsUpdatedMessageType
|
public enum ProjectVersionsUpdatedMessageType
|
||||||
{
|
{
|
||||||
CREATED,
|
CREATED,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ProjectVisibility
|
public enum ProjectVisibility
|
||||||
{
|
{
|
||||||
Private,
|
Private,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
// ReSharper disable InconsistentNaming
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum ResourceType
|
public enum ResourceType
|
||||||
{
|
{
|
||||||
commit,
|
commit,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// string based enum
|
||||||
|
/// </remarks>
|
||||||
public enum UserProjectsUpdatedMessageType
|
public enum UserProjectsUpdatedMessageType
|
||||||
{
|
{
|
||||||
ADDED,
|
ADDED,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
public record GenerateFileUploadUrlInput(string projectId, string fileName);
|
public record GenerateFileUploadUrlInput(string projectId, string fileName);
|
||||||
|
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public record StartFileImportInput(string projectId, string modelId, string fileId, string etag);
|
public record StartFileImportInput(string projectId, string modelId, string fileId, string etag);
|
||||||
|
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public record FileImportResult(
|
public record FileImportResult(
|
||||||
double durationSeconds,
|
double durationSeconds,
|
||||||
double downloadDurationSeconds,
|
double downloadDurationSeconds,
|
||||||
@@ -14,14 +16,23 @@ public record FileImportResult(
|
|||||||
|
|
||||||
public abstract class FileImportInputBase
|
public abstract class FileImportInputBase
|
||||||
{
|
{
|
||||||
|
internal const string FILE_IMPORT_DEPRECATION_MESSAGE =
|
||||||
|
"Part of the old API surface and will be removed in the future. Use the new ingestion API instead. Field will be deleted on June 1st, 2026";
|
||||||
|
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
|
protected FileImportInputBase() { }
|
||||||
|
|
||||||
public required string projectId { get; init; }
|
public required string projectId { get; init; }
|
||||||
public required string jobId { get; init; }
|
public required string jobId { get; init; }
|
||||||
public required IReadOnlyCollection<string> warnings { get; init; }
|
public required IReadOnlyCollection<string> warnings { get; init; }
|
||||||
|
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public required FileImportResult result { get; init; }
|
public required FileImportResult result { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1822 //Mark members as static
|
#pragma warning disable CA1822 //Mark members as static
|
||||||
|
|
||||||
|
[Obsolete(FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public sealed class FileImportSuccessInput() : FileImportInputBase()
|
public sealed class FileImportSuccessInput() : FileImportInputBase()
|
||||||
{
|
{
|
||||||
public const string TYPE_STATUS = "success";
|
public const string TYPE_STATUS = "success";
|
||||||
@@ -29,6 +40,7 @@ public sealed class FileImportSuccessInput() : FileImportInputBase()
|
|||||||
public string status => TYPE_STATUS;
|
public string status => TYPE_STATUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete(FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public sealed class FileImportErrorInput() : FileImportInputBase()
|
public sealed class FileImportErrorInput() : FileImportInputBase()
|
||||||
{
|
{
|
||||||
public const string TYPE_STATUS = "error";
|
public const string TYPE_STATUS = "error";
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Speckle.Newtonsoft.Json;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
|
|
||||||
|
public record SourceDataInput(
|
||||||
|
string sourceApplicationSlug,
|
||||||
|
string sourceApplicationVersion,
|
||||||
|
string? fileName,
|
||||||
|
long? fileSizeBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ModelIngestionCreateInput(
|
||||||
|
string modelId,
|
||||||
|
string projectId,
|
||||||
|
string progressMessage,
|
||||||
|
SourceDataInput sourceData,
|
||||||
|
int? maxIdleTimeoutSeconds = null
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
||||||
|
|
||||||
|
public record ModelIngestionSuccessInput(
|
||||||
|
string ingestionId,
|
||||||
|
string projectId,
|
||||||
|
string rootObjectId,
|
||||||
|
string? versionMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ModelIngestionFailedInput(
|
||||||
|
string ingestionId,
|
||||||
|
string projectId,
|
||||||
|
string errorReason,
|
||||||
|
string? errorStacktrace
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public static ModelIngestionFailedInput FromException(string ingestionId, string projectId, Exception ex)
|
||||||
|
{
|
||||||
|
return new ModelIngestionFailedInput(ingestionId, projectId, ex.Message, ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ModelIngestionCancelledInput(string ingestionId, string projectId, string cancellationMessage);
|
||||||
|
|
||||||
|
public record ModelIngestionStartProcessingInput(
|
||||||
|
string ingestionId,
|
||||||
|
string projectId,
|
||||||
|
string progressMessage,
|
||||||
|
SourceDataInput sourceData
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ModelIngestionRequeueInput(string ingestionId, string projectId, string progressMessage);
|
||||||
|
|
||||||
|
public record ProjectModelIngestionSubscriptionInput(
|
||||||
|
string projectId,
|
||||||
|
ModelIngestionReference ingestionReference,
|
||||||
|
[property: JsonIgnore] ProjectModelIngestionUpdatedMessageType messageType
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// The Newtonsoft serializer is setup to handle SCREAMING_CASE enums.
|
||||||
|
// But the API requires the enum to look exactly like they are
|
||||||
|
[JsonProperty(nameof(messageType))]
|
||||||
|
public string serializedType => messageType.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <c>@oneOf</c> i.e. server expects <b>either</b> <paramref name="ingestionId"/> or <paramref name="modelId"/>, but not both.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="ingestionId"></param>
|
||||||
|
/// <param name="modelId"></param>
|
||||||
|
public record ModelIngestionReference(string? ingestionId, string? modelId);
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
|
|
||||||
public record UpdateVersionInput(string versionId, string projectId, string? message);
|
public record UpdateVersionInput(string versionId, string projectId, string? message);
|
||||||
|
|
||||||
@@ -16,6 +18,10 @@ public record CreateVersionInput(
|
|||||||
IReadOnlyList<string>? parents = null
|
IReadOnlyList<string>? parents = null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <param name="versionId"></param>
|
||||||
|
/// <param name="projectId"></param>
|
||||||
|
/// <param name="sourceApplication">IMPORTANT: this is meant to be the slug of the application that has done the receiving, not to be confused with <see cref="Version.sourceApplication"/></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
public record MarkReceivedVersionInput(
|
public record MarkReceivedVersionInput(
|
||||||
string versionId,
|
string versionId,
|
||||||
string projectId,
|
string projectId,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
|
||||||
|
public sealed class ModelIngestion
|
||||||
|
{
|
||||||
|
public required string id { get; init; }
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ public sealed class PendingStreamCollaborator
|
|||||||
public string projectName { get; init; }
|
public string projectName { get; init; }
|
||||||
public string title { get; init; }
|
public string title { get; init; }
|
||||||
public string role { get; init; }
|
public string role { get; init; }
|
||||||
public LimitedUser invitedBy { get; init; }
|
public LimitedUser? invitedBy { get; init; }
|
||||||
public LimitedUser? user { get; init; }
|
public LimitedUser? user { get; init; }
|
||||||
public string? token { get; init; }
|
public string? token { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,7 @@ public sealed class ProjectPermissionChecks
|
|||||||
public PermissionCheckResult canCreateModel { get; init; }
|
public PermissionCheckResult canCreateModel { get; init; }
|
||||||
public PermissionCheckResult canDelete { get; init; }
|
public PermissionCheckResult canDelete { get; init; }
|
||||||
public PermissionCheckResult canLoad { get; init; }
|
public PermissionCheckResult canLoad { get; init; }
|
||||||
|
|
||||||
|
[Obsolete("Use ModelPermissionChecks.CanCreateVersion instead", true)]
|
||||||
public PermissionCheckResult canPublish { get; init; }
|
public PermissionCheckResult canPublish { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ namespace Speckle.Sdk.Api.GraphQL.Models;
|
|||||||
public sealed class UserProjectsUpdatedMessage : EventArgs
|
public sealed class UserProjectsUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public UserProjectsUpdatedMessageType type { get; init; }
|
public required UserProjectsUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public Project? project { get; init; }
|
public Project? project { get; init; }
|
||||||
}
|
}
|
||||||
@@ -17,10 +17,10 @@ public sealed class UserProjectsUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectCommentsUpdatedMessageType type { get; init; }
|
public required ProjectCommentsUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public Comment? comment { get; init; }
|
public Comment? comment { get; init; }
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,10 @@ public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectFileImportUpdatedMessageType type { get; init; }
|
public required ProjectFileImportUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public FileUpload? upload { get; init; }
|
public FileUpload? upload { get; init; }
|
||||||
}
|
}
|
||||||
@@ -39,10 +39,10 @@ public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectModelsUpdatedMessage : EventArgs
|
public sealed class ProjectModelsUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectModelsUpdatedMessageType type { get; init; }
|
public required ProjectModelsUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public Model? model { get; init; }
|
public Model? model { get; init; }
|
||||||
}
|
}
|
||||||
@@ -50,10 +50,10 @@ public sealed class ProjectModelsUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectPendingModelsUpdatedMessageType type { get; init; }
|
public required ProjectPendingModelsUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public FileUpload? model { get; init; }
|
public FileUpload? model { get; init; }
|
||||||
}
|
}
|
||||||
@@ -61,10 +61,10 @@ public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectUpdatedMessage : EventArgs
|
public sealed class ProjectUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectUpdatedMessageType type { get; init; }
|
public required ProjectUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
public Project? project { get; init; }
|
public Project? project { get; init; }
|
||||||
}
|
}
|
||||||
@@ -72,13 +72,22 @@ public sealed class ProjectUpdatedMessage : EventArgs
|
|||||||
public sealed class ProjectVersionsUpdatedMessage : EventArgs
|
public sealed class ProjectVersionsUpdatedMessage : EventArgs
|
||||||
{
|
{
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string id { get; init; }
|
public required string id { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public ProjectVersionsUpdatedMessageType type { get; init; }
|
public required ProjectVersionsUpdatedMessageType type { get; init; }
|
||||||
|
|
||||||
[JsonRequired]
|
[JsonRequired]
|
||||||
public string modelId { get; init; }
|
public required string modelId { get; init; }
|
||||||
|
|
||||||
public Version? version { get; init; }
|
public Version? version { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectModelIngestionUpdatedMessage : EventArgs
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required ModelIngestion modelIngestion { get; init; }
|
||||||
|
|
||||||
|
[JsonRequired]
|
||||||
|
public required ProjectModelIngestionUpdatedMessageType type { get; init; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Speckle.Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
|
||||||
public class LimitedWorkspace
|
public class LimitedWorkspace
|
||||||
@@ -6,8 +8,12 @@ public class LimitedWorkspace
|
|||||||
public string name { get; init; }
|
public string name { get; init; }
|
||||||
public string? role { get; init; }
|
public string? role { get; init; }
|
||||||
public string slug { get; init; }
|
public string slug { get; init; }
|
||||||
public string? logo { get; init; }
|
public string? logoUri { get; init; }
|
||||||
public string? description { get; init; }
|
public string? description { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Obsolete($"Deprecated, use {nameof(logoUri)} instead", true)]
|
||||||
|
public string? logo { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Workspace : LimitedWorkspace
|
public class Workspace : LimitedWorkspace
|
||||||
@@ -16,9 +22,13 @@ public class Workspace : LimitedWorkspace
|
|||||||
public DateTime updatedAt { get; init; }
|
public DateTime updatedAt { get; init; }
|
||||||
public bool readOnly { get; init; }
|
public bool readOnly { get; init; }
|
||||||
public WorkspacePermissionChecks permissions { get; init; }
|
public WorkspacePermissionChecks permissions { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Obsolete("Workspaces no longer have creation state, is always created true", true)]
|
||||||
public WorkspaceCreationState? creationState { get; init; }
|
public WorkspaceCreationState? creationState { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete("Workspaces no longer have creation state, is always created true")]
|
||||||
public sealed class WorkspaceCreationState
|
public sealed class WorkspaceCreationState
|
||||||
{
|
{
|
||||||
public bool completed { get; init; }
|
public bool completed { get; init; }
|
||||||
|
|||||||
@@ -264,15 +264,11 @@ public sealed class ActiveUserResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
logoUrl
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
readOnly
|
readOnly
|
||||||
description
|
description
|
||||||
creationState
|
|
||||||
{
|
|
||||||
completed
|
|
||||||
}
|
|
||||||
permissions {
|
permissions {
|
||||||
canCreateProject {
|
canCreateProject {
|
||||||
authorized
|
authorized
|
||||||
@@ -317,7 +313,7 @@ public sealed class ActiveUserResource
|
|||||||
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
||||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||||
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
private async Task<LimitedWorkspace?> GetActiveWorkspace_Legacy(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
//language=graphql
|
//language=graphql
|
||||||
const string QUERY = """
|
const string QUERY = """
|
||||||
@@ -328,7 +324,6 @@ public sealed class ActiveUserResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
|
||||||
description
|
description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +344,47 @@ public sealed class ActiveUserResource
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
query ActiveUser {
|
||||||
|
data:activeUser {
|
||||||
|
data:activeWorkspace {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
role
|
||||||
|
slug
|
||||||
|
logoUrl
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var request = new GraphQLRequest { Query = QUERY };
|
||||||
|
|
||||||
|
NullableResponse<NullableResponse<LimitedWorkspace?>?> response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await _client
|
||||||
|
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<LimitedWorkspace?>?>>(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (SpeckleGraphQLInvalidQueryException)
|
||||||
|
{
|
||||||
|
//v2.x.x servers do not have a logoUrl property
|
||||||
|
return await GetActiveWorkspace_Legacy(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data is null)
|
||||||
|
{
|
||||||
|
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
/// <param name="limit">Max number of projects to fetch</param>
|
/// <param name="limit">Max number of projects to fetch</param>
|
||||||
/// <param name="cursor">Optional cursor for pagination</param>
|
/// <param name="cursor">Optional cursor for pagination</param>
|
||||||
/// <param name="filter">Optional filter</param>
|
/// <param name="filter">Optional filter</param>
|
||||||
@@ -397,11 +433,6 @@ public sealed class ActiveUserResource
|
|||||||
authorized
|
authorized
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
canPublish {
|
|
||||||
code
|
|
||||||
authorized
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ public sealed class FileImportResource : IDisposable
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Only use this if you are writing a file importer, that is responsible for
|
/// Only use this if you are writing a file importer, that is responsible for
|
||||||
/// processing file import jobs.
|
/// processing file import jobs.
|
||||||
/// Only works on servers version >=2.25.8
|
/// Only works on servers version >=2.25.8 but from 3.0.7 onwards has been deprecated and replaced by model ingestion api
|
||||||
|
/// see <see cref="ModelIngestionResource.Complete"/>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public async Task<bool> FinishFileImportJob(FileImportInputBase input, CancellationToken cancellationToken)
|
public async Task<bool> FinishFileImportJob(FileImportInputBase input, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
//language=graphql
|
//language=graphql
|
||||||
@@ -57,7 +59,11 @@ public sealed class FileImportResource : IDisposable
|
|||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
/// <remarks>
|
||||||
|
/// Only works on servers version >=2.25.8 but from 3.0.7 onwards has been deprecated and replaced by model ingestion api
|
||||||
|
/// see <see cref="ModelIngestionResource.StartProcessing"/>
|
||||||
|
/// </remarks>
|
||||||
|
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||||
public async Task<FileImport> StartFileImportJob(
|
public async Task<FileImport> StartFileImportJob(
|
||||||
StartFileImportInput input,
|
StartFileImportInput input,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
|
|||||||
@@ -0,0 +1,506 @@
|
|||||||
|
using GraphQL;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
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</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ModelIngestionResource
|
||||||
|
{
|
||||||
|
private readonly ISpeckleGraphQLClient _client;
|
||||||
|
|
||||||
|
internal ModelIngestionResource(ISpeckleGraphQLClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new model ingestion
|
||||||
|
/// </summary>
|
||||||
|
/// <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</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> Create(
|
||||||
|
ModelIngestionCreateInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionCreate($input: ModelIngestionCreateInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: create(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="modelIngestionId"></param>
|
||||||
|
/// <param name="projectId"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> Get(
|
||||||
|
string modelIngestionId,
|
||||||
|
string projectId,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
query Query($projectId: String!, $modelIngestionId: ID!) {
|
||||||
|
data:project(id: $projectId) {
|
||||||
|
data:ingestion(id: $modelIngestionId) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
... on ModelIngestionSuccessStatus
|
||||||
|
{
|
||||||
|
versionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelIngestionId } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<ModelIngestion>>>(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For File Import / Cloud integrations only
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> StartProcessing(
|
||||||
|
ModelIngestionStartProcessingInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionStartProcessing($input: ModelIngestionStartProcessingInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: startProcessing(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For File Import / Cloud integrations only
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> Requeue(
|
||||||
|
ModelIngestionRequeueInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionStartProcessing($input: ModelIngestionRequeueInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: requeue(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> UpdateProgress(
|
||||||
|
ModelIngestionUpdateInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionUpdateProgress(
|
||||||
|
$input: ModelIngestionUpdateInput!
|
||||||
|
) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: updateProgress(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request that the server completes the ingestion by creating a version
|
||||||
|
/// 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</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="FailWithError"/>
|
||||||
|
/// <seealso cref="FailWithCancel"/>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns>The version id</returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<string> Complete(ModelIngestionSuccessInput input, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionComplete($input: ModelIngestionSuccessInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: completeWithVersion(input: $input) {
|
||||||
|
data:statusData {
|
||||||
|
... on ModelIngestionSuccessStatus {
|
||||||
|
data:versionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<
|
||||||
|
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<string>>>>>
|
||||||
|
>(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fail the job with an error.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead<br/>
|
||||||
|
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="FailWithCancel"/>
|
||||||
|
/// <seealso cref="Complete"/>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> FailWithError(
|
||||||
|
ModelIngestionFailedInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionFailWithError($input: ModelIngestionFailedInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: failWithError(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fail the ingestion with a <c>canceled</c> status.
|
||||||
|
/// 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</c> and above
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="FailWithError"/>
|
||||||
|
/// <seealso cref="Complete"/>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> FailWithCancel(
|
||||||
|
ModelIngestionCancelledInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionFailWithCancel($input: ModelIngestionCancelledInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: failWithCancel(input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request that the <see cref="ModelIngestion"/> is canceled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Note simply calling this mutation does not imediatly cancel, it doesn't even guarantee it will be canceled at all.
|
||||||
|
/// It's up to the client to observe this cancellation request
|
||||||
|
/// 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</c> and above
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="FailWithError"/>
|
||||||
|
/// <seealso cref="Complete"/>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
|
public async Task<ModelIngestion> RequestCancellation(
|
||||||
|
ModelIngestionCancelledInput input,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
mutation IngestionRequestCancellation($input: ModelIngestionRequestCancellationInput!) {
|
||||||
|
data: projectMutations {
|
||||||
|
data: modelIngestionMutations {
|
||||||
|
data: requestCancellation (input: $input) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
var res = await _client
|
||||||
|
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||||
|
request,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return res.data.data.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -312,4 +312,88 @@ public sealed class ModelResource
|
|||||||
|
|
||||||
return res.data.data;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ public sealed class OtherUserResource
|
|||||||
/// <param name="query">String to search for. Must be at least 3 characters</param>
|
/// <param name="query">String to search for. Must be at least 3 characters</param>
|
||||||
/// <param name="limit">Max number of users to fetch</param>
|
/// <param name="limit">Max number of users to fetch</param>
|
||||||
/// <param name="cursor">Optional cursor for pagination</param>
|
/// <param name="cursor">Optional cursor for pagination</param>
|
||||||
/// <param name="archived"></param>
|
|
||||||
/// <param name="emailOnly"></param>
|
/// <param name="emailOnly"></param>
|
||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -61,26 +60,25 @@ public sealed class OtherUserResource
|
|||||||
string query,
|
string query,
|
||||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||||
string? cursor = null,
|
string? cursor = null,
|
||||||
bool archived = false,
|
|
||||||
bool emailOnly = false,
|
bool emailOnly = false,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//language=graphql
|
//language=graphql
|
||||||
const string QUERY = """
|
const string QUERY = """
|
||||||
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) {
|
query Users($input: UsersRetrievalInput!) {
|
||||||
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) {
|
data:users(input: $input) {
|
||||||
cursor
|
cursor
|
||||||
items {
|
items {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
company
|
company
|
||||||
avatar
|
avatar
|
||||||
verified
|
verified
|
||||||
role
|
role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -89,11 +87,13 @@ public sealed class OtherUserResource
|
|||||||
Query = QUERY,
|
Query = QUERY,
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
query,
|
input = new
|
||||||
limit,
|
{
|
||||||
cursor,
|
query,
|
||||||
archived,
|
limit,
|
||||||
emailOnly,
|
emailOnly,
|
||||||
|
cursor,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using GraphQL;
|
using GraphQL;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
using Speckle.Sdk.Api.GraphQL.Models;
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||||
@@ -75,6 +76,7 @@ public sealed class SubscriptionResource : IDisposable
|
|||||||
/// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary>
|
/// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary>
|
||||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||||
|
[Obsolete("Comments are now issues, and we've not update SDKs with the new subs")]
|
||||||
public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription(
|
public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription(
|
||||||
ViewerUpdateTrackingTarget target
|
ViewerUpdateTrackingTarget target
|
||||||
)
|
)
|
||||||
@@ -212,6 +214,66 @@ public sealed class SubscriptionResource : IDisposable
|
|||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||||
|
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||||
|
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionUpdatedSubscription(
|
||||||
|
ProjectModelIngestionSubscriptionInput input
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
subscription IngestionUpdated($input: ProjectModelIngestionSubscriptionInput!) {
|
||||||
|
data: projectModelIngestionUpdated(input: $input) {
|
||||||
|
modelIngestion {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
modelId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
cancellationRequested
|
||||||
|
statusData {
|
||||||
|
... on HasModelIngestionStatus {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
... on HasProgressMessage {
|
||||||
|
progressMessage
|
||||||
|
}
|
||||||
|
... on ModelIngestionSuccessStatus
|
||||||
|
{
|
||||||
|
versionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||||
|
|
||||||
|
Subscription<ProjectModelIngestionUpdatedMessage> subscription = new(_client, request);
|
||||||
|
_subscriptions.Add(subscription);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||||
|
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||||
|
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||||
|
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionCancellationRequestedSubscription(
|
||||||
|
string ingestionId,
|
||||||
|
string projectId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return CreateProjectModelIngestionUpdatedSubscription(
|
||||||
|
new ProjectModelIngestionSubscriptionInput(
|
||||||
|
projectId,
|
||||||
|
new(ingestionId, null),
|
||||||
|
ProjectModelIngestionUpdatedMessageType.cancellationRequested
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var subscription in _subscriptions)
|
foreach (var subscription in _subscriptions)
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ public sealed class WorkspaceResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
logoUrl
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
readOnly
|
readOnly
|
||||||
description
|
description
|
||||||
creationState
|
|
||||||
{
|
|
||||||
completed
|
|
||||||
}
|
|
||||||
permissions {
|
permissions {
|
||||||
canCreateProject {
|
canCreateProject {
|
||||||
authorized
|
authorized
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
schema: https://app.speckle.systems/graphql
|
schema: https://latest.speckle.systems/graphql
|
||||||
documents: '**/*.graphql'
|
documents: '**/*.graphql'
|
||||||
|
|||||||
@@ -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);
|
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()
|
public static void Reset()
|
||||||
{
|
{
|
||||||
s_availableTypes = new();
|
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 sealed class NullActivityFactory : ISdkActivityFactory
|
||||||
{
|
{
|
||||||
public void Dispose() { }
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ namespace Speckle.Sdk.Models;
|
|||||||
public enum DynamicBaseMemberType
|
public enum DynamicBaseMemberType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed members of the DynamicBase object
|
/// The typed members of the <see cref="DynamicBase"/> object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Instance = 1,
|
Instance = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The dynamically added members of the DynamicBase object
|
/// The dynamically added members of the <see cref="DynamicBase"/> object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Dynamic = 2,
|
Dynamic = 2,
|
||||||
|
|
||||||
@@ -22,8 +22,9 @@ public enum DynamicBaseMemberType
|
|||||||
Obsolete = 4,
|
Obsolete = 4,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed methods flagged with TODO:
|
/// Old feature supported in v2 for grasshopper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Feature no longer supported")]
|
||||||
SchemaComputed = 16,
|
SchemaComputed = 16,
|
||||||
|
|
||||||
/// <summary>
|
/// <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,85 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
using var writer = new StreamWriter(gzip);
|
||||||
|
|
||||||
|
await foreach (var item in _channel.ReadAllAsync(_cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await writer.WriteLineAsync($"{item.Id}\t{item.SpeckleType}\t{item.Json}").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
await writer.FlushAsync(_cancellationToken).ConfigureAwait(false);
|
||||||
|
#else
|
||||||
|
await writer.FlushAsync().ConfigureAwait(false);
|
||||||
|
#endif
|
||||||
|
tempFile.FileInfo.Refresh();
|
||||||
|
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
tempFile.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UploadItem _lastItem;
|
||||||
|
|
||||||
|
public async Task<ObjectReference> Process(Base @base)
|
||||||
|
{
|
||||||
|
var results = _serializer.Serialize(@base).ToArray();
|
||||||
|
var first = results.First();
|
||||||
|
foreach (var item in results)
|
||||||
|
{
|
||||||
|
// we're not doing fire and forget here so that we get the backpressure from the uploader
|
||||||
|
await _diskStore.PushAsync(item).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this is important to keep track of. When we serialze an object, we get back a list of objects, with the first one being the original root.
|
||||||
|
// In the case of the commit root object, this means the last object is not necessarily the root; we therefore need to manually track its existance here
|
||||||
|
// and ensure it's the last one through in the uploader's stream. See WaitForUpload down below.
|
||||||
|
_lastItem = first;
|
||||||
|
return 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,351 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using Speckle.DoubleNumerics;
|
||||||
|
using Speckle.Newtonsoft.Json;
|
||||||
|
using Speckle.Sdk.Dependencies;
|
||||||
|
using Speckle.Sdk.Helpers;
|
||||||
|
using Speckle.Sdk.Models;
|
||||||
|
using Speckle.Sdk.Serialisation;
|
||||||
|
|
||||||
|
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, Json, 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(JsonIgnoreAttribute), false))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = prop.GetValue(baseObj);
|
||||||
|
var isDetachable = prop.GetCustomAttribute<DetachPropertyAttribute>(true)?.Detachable ?? false;
|
||||||
|
|
||||||
|
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, Json) SerializeBase(
|
||||||
|
Base baseObj,
|
||||||
|
bool forceDetach,
|
||||||
|
Dictionary<string, int> closures,
|
||||||
|
List<(Id, Json, Dictionary<string, int>, Base, string)> detachedObjects
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var childClosures = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
var sb = Pools.StringBuilders.Get();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stringWriter = new StringWriter(sb);
|
||||||
|
using var jsonWriter = new JsonTextWriter(stringWriter);
|
||||||
|
using var idWriter = new SerializerIdWriter(jsonWriter);
|
||||||
|
|
||||||
|
idWriter.WriteStartObject();
|
||||||
|
|
||||||
|
foreach (var prop in ExtractProperties(baseObj))
|
||||||
|
{
|
||||||
|
idWriter.WritePropertyName(prop.Name);
|
||||||
|
SerializeValue(prop.Value, idWriter, prop.IsDetachable, childClosures, detachedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (jsonForId, finalWriter) = idWriter.FinishIdWriter();
|
||||||
|
var id = IdGenerator.ComputeId(jsonForId);
|
||||||
|
|
||||||
|
finalWriter.WritePropertyName("id");
|
||||||
|
finalWriter.WriteValue(id.Value);
|
||||||
|
|
||||||
|
baseObj.id = id.Value;
|
||||||
|
|
||||||
|
if ((forceDetach || childClosures.Count > 0) && childClosures.Count > 0)
|
||||||
|
{
|
||||||
|
finalWriter.WritePropertyName("__closure");
|
||||||
|
finalWriter.WriteStartObject();
|
||||||
|
foreach (var kvp in childClosures)
|
||||||
|
{
|
||||||
|
finalWriter.WritePropertyName(kvp.Key);
|
||||||
|
finalWriter.WriteValue(kvp.Value);
|
||||||
|
}
|
||||||
|
finalWriter.WriteEndObject();
|
||||||
|
|
||||||
|
foreach (var kvp in childClosures)
|
||||||
|
{
|
||||||
|
closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existing) ? existing + kvp.Value : kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalWriter.WriteEndObject();
|
||||||
|
finalWriter.Flush();
|
||||||
|
|
||||||
|
var json = new Json(stringWriter.ToString());
|
||||||
|
return (id, json);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Pools.StringBuilders.Return(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SerializeValue(
|
||||||
|
object? value,
|
||||||
|
JsonWriter writer,
|
||||||
|
bool isDetachable,
|
||||||
|
Dictionary<string, int> closures,
|
||||||
|
List<(Id, Json, Dictionary<string, int>, Base, string)> detachedObjects
|
||||||
|
)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case Enum:
|
||||||
|
writer.WriteValue((int)value);
|
||||||
|
return;
|
||||||
|
case Guid g:
|
||||||
|
writer.WriteValue(g.ToString());
|
||||||
|
return;
|
||||||
|
case Color c:
|
||||||
|
writer.WriteValue(c.ToArgb());
|
||||||
|
return;
|
||||||
|
case DateTime dt:
|
||||||
|
writer.WriteValue(dt.ToString("o", CultureInfo.InvariantCulture));
|
||||||
|
return;
|
||||||
|
case Matrix4x4 md:
|
||||||
|
writer.WriteStartArray();
|
||||||
|
writer.WriteValue(md.M11);
|
||||||
|
writer.WriteValue(md.M12);
|
||||||
|
writer.WriteValue(md.M13);
|
||||||
|
writer.WriteValue(md.M14);
|
||||||
|
writer.WriteValue(md.M21);
|
||||||
|
writer.WriteValue(md.M22);
|
||||||
|
writer.WriteValue(md.M23);
|
||||||
|
writer.WriteValue(md.M24);
|
||||||
|
writer.WriteValue(md.M31);
|
||||||
|
writer.WriteValue(md.M32);
|
||||||
|
writer.WriteValue(md.M33);
|
||||||
|
writer.WriteValue(md.M34);
|
||||||
|
writer.WriteValue(md.M41);
|
||||||
|
writer.WriteValue(md.M42);
|
||||||
|
writer.WriteValue(md.M43);
|
||||||
|
writer.WriteValue(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.WritePropertyName("speckle_type");
|
||||||
|
writer.WriteValue("reference");
|
||||||
|
writer.WritePropertyName("referencedId");
|
||||||
|
writer.WriteValue(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.WritePropertyName("speckle_type");
|
||||||
|
writer.WriteValue("reference");
|
||||||
|
writer.WritePropertyName("referencedId");
|
||||||
|
writer.WriteValue(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.Value);
|
||||||
|
|
||||||
|
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
|
||||||
|
writer.WriteValue(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UploadItem ReferenceToUploadItem(ObjectReference existingRef)
|
||||||
|
{
|
||||||
|
var sb = Pools.StringBuilders.Get();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stringWriter = new StringWriter(sb);
|
||||||
|
using var jsonWriter = new JsonTextWriter(stringWriter);
|
||||||
|
|
||||||
|
jsonWriter.WriteStartObject();
|
||||||
|
jsonWriter.WritePropertyName("speckle_type");
|
||||||
|
jsonWriter.WriteValue("reference");
|
||||||
|
jsonWriter.WritePropertyName("referencedId");
|
||||||
|
jsonWriter.WriteValue(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.WriteValue(kvp.Value);
|
||||||
|
}
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
jsonWriter.WriteNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
jsonWriter.Flush();
|
||||||
|
|
||||||
|
var refJson = new Json(stringWriter.ToString());
|
||||||
|
|
||||||
|
return new UploadItem(
|
||||||
|
existingRef.referencedId,
|
||||||
|
refJson,
|
||||||
|
existingRef.speckle_type,
|
||||||
|
existingRef // Pass through the original ObjectReference
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Pools.StringBuilders.Return(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> UploadToS3(Stream fileStream, PresignedUploadResponse presignedUploadResponse)
|
||||||
|
{
|
||||||
|
using var a = _activity.Start("Uploading file to pre-signed url");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TriggerProcessing(TriggerUploadRequest request)
|
||||||
|
{
|
||||||
|
using var a = _activity.Start("Triggering Processing");
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_speckleClient.Dispose();
|
||||||
|
_s3Client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Speckle.Newtonsoft.Json;
|
||||||
|
using Speckle.Sdk.Models;
|
||||||
|
using Speckle.Sdk.Serialisation;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Pipelines.Send;
|
||||||
|
|
||||||
|
public record UploadItem(string Id, Json Json, string SpeckleType, ObjectReference Reference);
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -2,7 +2,13 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Serialisation.V2.Send;
|
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);
|
public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value);
|
||||||
|
|
||||||
|
|||||||
@@ -113,16 +113,6 @@ public sealed class SerializeProcess(
|
|||||||
_processSource.Token
|
_processSource.Token
|
||||||
);
|
);
|
||||||
var findTotalObjectsTask = Task.CompletedTask;
|
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);
|
await Traverse(root).ConfigureAwait(false);
|
||||||
ThrowIfFailed();
|
ThrowIfFailed();
|
||||||
@@ -133,6 +123,7 @@ public sealed class SerializeProcess(
|
|||||||
ThrowIfFailed();
|
ThrowIfFailed();
|
||||||
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
||||||
ThrowIfFailed();
|
ThrowIfFailed();
|
||||||
|
|
||||||
return new(root.id.NotNull(), baseSerializer.ObjectReferences.Freeze());
|
return new(root.id.NotNull(), baseSerializer.ObjectReferences.Freeze());
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Speckle.Sdk.Dependencies;
|
|||||||
using Speckle.Sdk.Host;
|
using Speckle.Sdk.Host;
|
||||||
using Speckle.Sdk.Logging;
|
using Speckle.Sdk.Logging;
|
||||||
using Speckle.Sdk.Models.GraphTraversal;
|
using Speckle.Sdk.Models.GraphTraversal;
|
||||||
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
using Speckle.Sdk.Serialisation.V2;
|
using Speckle.Sdk.Serialisation.V2;
|
||||||
using Speckle.Sdk.Serialisation.V2.Receive;
|
using Speckle.Sdk.Serialisation.V2.Receive;
|
||||||
using Speckle.Sdk.Serialisation.V2.Send;
|
using Speckle.Sdk.Serialisation.V2.Send;
|
||||||
@@ -96,7 +97,8 @@ public static class ServiceRegistration
|
|||||||
typeof(DeserializeProcess),
|
typeof(DeserializeProcess),
|
||||||
typeof(ObjectLoader),
|
typeof(ObjectLoader),
|
||||||
typeof(TraversalRule),
|
typeof(TraversalRule),
|
||||||
typeof(Client)
|
typeof(Client),
|
||||||
|
typeof(IngestionProgressManager)
|
||||||
);
|
);
|
||||||
serviceCollection.AddMatchingInterfacesAsTransient(typeof(GraphQLRetry).Assembly);
|
serviceCollection.AddMatchingInterfacesAsTransient(typeof(GraphQLRetry).Assembly);
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<PackageReference Include="Microsoft.CSharp" />
|
<PackageReference Include="Microsoft.CSharp" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Speckle.Sdk.Dependencies\Speckle.Sdk.Dependencies.csproj" />
|
<ProjectReference Include="..\Speckle.Sdk.Dependencies\Speckle.Sdk.Dependencies.csproj" />
|
||||||
|
|||||||
@@ -13,15 +13,6 @@
|
|||||||
"System.Reactive": "5.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": {
|
"Microsoft.CSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.7.0, )",
|
"requested": "[4.7.0, )",
|
||||||
@@ -291,7 +282,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speckle.sdk.dependencies": {
|
"speckle.sdk.dependencies": {
|
||||||
"type": "Project"
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": "[9.0.4, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[9.0.4, )",
|
||||||
|
"resolved": "9.0.4",
|
||||||
|
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net8.0": {
|
"net8.0": {
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Bcl.AsyncInterfaces": {
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[5.0.0, )",
|
"requested": "[9.0.4, )",
|
||||||
"resolved": "1.1.0",
|
"resolved": "1.1.0",
|
||||||
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ public class SerializationTests
|
|||||||
idToBase.Count.Should().Be(count);
|
idToBase.Count.Should().Be(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory(Skip = "Takes too long")]
|
||||||
[InlineData(1)]
|
[InlineData(1)]
|
||||||
[InlineData(4)]
|
[InlineData(4)]
|
||||||
public async Task Roundtrip_Test_New(int concurrency)
|
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);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
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(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -91,7 +91,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
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(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -132,7 +132,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
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(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -171,7 +171,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
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(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using Speckle.Sdk.Api;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||||
|
|
||||||
|
[Trait("Server", "Internal")]
|
||||||
|
public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private IClient _testUser;
|
||||||
|
private ModelIngestionResource Sut => _testUser.Ingestion;
|
||||||
|
private Project _project;
|
||||||
|
private Model _model;
|
||||||
|
|
||||||
|
public Task DisposeAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_testUser = await Fixtures.SeedUserWithClient();
|
||||||
|
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||||
|
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateIngestionNonExistentProject()
|
||||||
|
{
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
"Doesn't exist...",
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||||
|
{
|
||||||
|
_ = await Sut.Create(createInput);
|
||||||
|
});
|
||||||
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLStreamNotFoundException>(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateNonExistentNonExistent()
|
||||||
|
{
|
||||||
|
var updateInput = new ModelIngestionUpdateInput("Doesn't exist", _project.id, "Can't be", 0.5);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||||
|
{
|
||||||
|
_ = await Sut.UpdateProgress(updateInput);
|
||||||
|
});
|
||||||
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CancelNonExistentIngestion()
|
||||||
|
{
|
||||||
|
var input = new ModelIngestionCancelledInput(
|
||||||
|
"Non-existent-ingestion",
|
||||||
|
_project.id,
|
||||||
|
cancellationMessage: "This was cancelled for testing purposes"
|
||||||
|
);
|
||||||
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||||
|
{
|
||||||
|
_ = await Sut.FailWithCancel(input);
|
||||||
|
});
|
||||||
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
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.Models;
|
||||||
|
using Speckle.Sdk.Transports;
|
||||||
|
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||||
|
|
||||||
|
[Trait("Server", "Internal")]
|
||||||
|
public sealed class ModelIngestionResourceTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private IClient _testUser;
|
||||||
|
private ModelIngestionResource Sut => _testUser.Ingestion;
|
||||||
|
private Project _project;
|
||||||
|
private Model _model;
|
||||||
|
private IOperations _operations;
|
||||||
|
|
||||||
|
public Task DisposeAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
|
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||||
|
|
||||||
|
_testUser = await Fixtures.SeedUserWithClient();
|
||||||
|
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||||
|
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAndError()
|
||||||
|
{
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
ModelIngestion ingest = await Sut.Create(createInput);
|
||||||
|
|
||||||
|
var errorInput = new ModelIngestionFailedInput(ingest.id, _project.id, "A bad thing happened", "Over hear!");
|
||||||
|
var res = await Sut.FailWithError(errorInput);
|
||||||
|
Assert.Equal(ingest.id, res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAndUpdate()
|
||||||
|
{
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
ModelIngestion ingest = await Sut.Create(createInput);
|
||||||
|
|
||||||
|
await Update(null, "None");
|
||||||
|
await Update(0.1, "0.1");
|
||||||
|
await Update(0.5, "Whoa-oh! We're half way there!");
|
||||||
|
await Update(1, "Finished");
|
||||||
|
await Update(0.2, "Back to processing again");
|
||||||
|
|
||||||
|
async Task Update(double? progress, string message)
|
||||||
|
{
|
||||||
|
var updateInput = new ModelIngestionUpdateInput(ingest.id, _project.id, message, progress);
|
||||||
|
var res = await Sut.UpdateProgress(updateInput);
|
||||||
|
|
||||||
|
Assert.Equal(message, res.statusData.progressMessage);
|
||||||
|
Assert.False(res.cancellationRequested);
|
||||||
|
Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAndCancel()
|
||||||
|
{
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
ModelIngestion ingest = await Sut.Create(createInput);
|
||||||
|
|
||||||
|
var input = new ModelIngestionCancelledInput(
|
||||||
|
ingest.id,
|
||||||
|
_project.id,
|
||||||
|
cancellationMessage: "This was cancelled for testing purposes"
|
||||||
|
);
|
||||||
|
var res = await Sut.FailWithCancel(input);
|
||||||
|
Assert.Equal(ingest.id, res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAndComplete()
|
||||||
|
{
|
||||||
|
ModelIngestionCreateInput createInput = new(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
ModelIngestion ingest = await Sut.Create(createInput);
|
||||||
|
|
||||||
|
Base myObject = Fixtures.GenerateNestedObject();
|
||||||
|
var sendResult = await _operations.Send2(
|
||||||
|
_testUser.ServerUrl,
|
||||||
|
_project.id,
|
||||||
|
_testUser.Account.token,
|
||||||
|
myObject,
|
||||||
|
new Progress<ProgressArgs>(x =>
|
||||||
|
{
|
||||||
|
var updateInput = new ModelIngestionUpdateInput(
|
||||||
|
ingest.id,
|
||||||
|
_project.id,
|
||||||
|
$"{x.Count} / {x.Total}",
|
||||||
|
x.Total == null ? null : x.Count / x.Total
|
||||||
|
);
|
||||||
|
_ = Sut.UpdateProgress(updateInput).Result;
|
||||||
|
}),
|
||||||
|
CancellationToken.None,
|
||||||
|
new(true, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
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]
|
||||||
|
public async Task CreateAndGet()
|
||||||
|
{
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
ModelIngestion ingest = await Sut.Create(createInput);
|
||||||
|
|
||||||
|
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]
|
||||||
|
public async Task TestRequeue()
|
||||||
|
{
|
||||||
|
//Not sure if is desirable that ingestions created by the modelIngestionMutations.create mutation can be re-queued
|
||||||
|
//But the server allows it, so we test it
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
var ingestion = await Sut.Create(createInput);
|
||||||
|
var res = await Sut.Requeue(new(ingestion.id, _project.id, "we'll try and requeue this ingestion"));
|
||||||
|
|
||||||
|
Assert.Equal(ingestion.id, res.id);
|
||||||
|
Assert.Equal(ModelIngestionStatus.queued, res.statusData.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TestStartProcessing()
|
||||||
|
{
|
||||||
|
//Not sure if is desirable that StartProcessing can be used by ingestions created by the modelIngestionMutations.create mutation
|
||||||
|
//But the server allows it, so we test it
|
||||||
|
var createInput = new ModelIngestionCreateInput(
|
||||||
|
_model.id,
|
||||||
|
_project.id,
|
||||||
|
"Starting processing",
|
||||||
|
new(".NET test runner", "0.0.0", null, null)
|
||||||
|
);
|
||||||
|
var ingestion = await Sut.Create(createInput);
|
||||||
|
var res = await Sut.StartProcessing(
|
||||||
|
new(ingestion.id, _project.id, "", new SourceDataInput("what", "happens", "now", 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Equal(ingestion.id, res.id);
|
||||||
|
Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Speckle.Sdk.Api;
|
using Speckle.Sdk.Api;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
using Speckle.Sdk.Api.GraphQL.Models;
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||||
@@ -17,7 +18,7 @@ public class ModelResourceTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
// Runs instead of [SetUp] in NUnit
|
// Runs instead of [SetUp] in NUnit
|
||||||
_testUser = await Fixtures.SeedUserWithClient();
|
_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));
|
_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>();
|
var delEx = await FluentActions.Invoking(() => Sut.Delete(input)).Should().ThrowAsync<AggregateException>();
|
||||||
getEx.WithInnerExceptionExactly<SpeckleGraphQLException>();
|
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);
|
ProjectUpdateRoleInput input = new(_secondUser.Account.id.NotNull(), "NonExistentProject", newRole);
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.UpdateRole(input));
|
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]
|
[Theory]
|
||||||
|
|||||||
@@ -103,8 +103,30 @@ public class ProjectResourceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestUserHasProjectPermissions()
|
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.canCreateModel.authorized.Should().Be(true);
|
||||||
res.canDelete.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-7
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Speckle.Sdk.Api;
|
using Speckle.Sdk.Api;
|
||||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||||
@@ -13,9 +13,9 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
private const int WAIT_PERIOD = 3000; // 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
|
#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
|
#endif
|
||||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 400;
|
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 600;
|
||||||
private IClient _testUser;
|
private IClient _testUser;
|
||||||
private Project _testProject;
|
private Project _testProject;
|
||||||
private Model _testModel;
|
private Model _testModel;
|
||||||
@@ -32,6 +32,7 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
|||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
_testUser = await Fixtures.SeedUserWithClient();
|
_testUser = await Fixtures.SeedUserWithClient();
|
||||||
|
await _testUser.InitializeWebsocket();
|
||||||
_testProject = await _testUser.Project.Create(new("test project123", "desc", null));
|
_testProject = await _testUser.Project.Create(new("test project123", "desc", null));
|
||||||
_testModel = await _testUser.Model.Create(new("test model", "desc", _testProject.id));
|
_testModel = await _testUser.Model.Create(new("test model", "desc", _testProject.id));
|
||||||
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
|
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
|
||||||
@@ -80,15 +81,15 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
|||||||
public async Task ProjectUpdated_SubscriptionIsCalled()
|
public async Task ProjectUpdated_SubscriptionIsCalled()
|
||||||
{
|
{
|
||||||
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
|
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
|
||||||
using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
|
using Subscription<ProjectUpdatedMessage> sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
|
||||||
sub.Listeners += (_, message) => tcs.SetResult(message);
|
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||||
|
|
||||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||||
|
|
||||||
var input = new ProjectUpdateInput(_testProject.id, "This is my new name");
|
ProjectUpdateInput input = new(_testProject.id, "This is my new name");
|
||||||
var created = await _testUser.Project.Update(input);
|
Project created = await _testUser.Project.Update(input);
|
||||||
|
|
||||||
var subscriptionMessage = await tcs.Task;
|
ProjectUpdatedMessage subscriptionMessage = await tcs.Task;
|
||||||
|
|
||||||
subscriptionMessage.Should().NotBeNull();
|
subscriptionMessage.Should().NotBeNull();
|
||||||
subscriptionMessage.id.Should().Be(created.id);
|
subscriptionMessage.id.Should().Be(created.id);
|
||||||
@@ -135,4 +136,74 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
|||||||
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
|
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
|
||||||
subscriptionMessage.comment.Should().NotBeNull();
|
subscriptionMessage.comment.Should().NotBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||||
|
public async Task ProjectModelIngestionCancellationRequested_SubscriptionIsCalled()
|
||||||
|
{
|
||||||
|
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||||
|
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||||
|
);
|
||||||
|
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||||
|
|
||||||
|
using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.id);
|
||||||
|
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||||
|
|
||||||
|
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||||
|
|
||||||
|
await _testUser.Ingestion.RequestCancellation(new(ingestion.id, _testProject.id, "please cancel"));
|
||||||
|
|
||||||
|
var subscriptionMessage = await tcs.Task;
|
||||||
|
|
||||||
|
subscriptionMessage.Should().NotBeNull();
|
||||||
|
subscriptionMessage.type.Should().Be(ProjectModelIngestionUpdatedMessageType.cancellationRequested);
|
||||||
|
subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||||
|
public async Task ProjectModelIngestionUpdate_UpdateSubscriptionIs()
|
||||||
|
{
|
||||||
|
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||||
|
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||||
|
);
|
||||||
|
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||||
|
|
||||||
|
using var sub = Sut.CreateProjectModelIngestionUpdatedSubscription(
|
||||||
|
new(
|
||||||
|
_testProject.id,
|
||||||
|
new ModelIngestionReference(ingestion.id, null),
|
||||||
|
ProjectModelIngestionUpdatedMessageType.updated
|
||||||
|
)
|
||||||
|
);
|
||||||
|
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||||
|
|
||||||
|
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||||
|
|
||||||
|
await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "Here's an update", 0.314));
|
||||||
|
|
||||||
|
var subscriptionMessage = await tcs.Task;
|
||||||
|
|
||||||
|
subscriptionMessage.Should().NotBeNull();
|
||||||
|
subscriptionMessage.type.Should().Be(ProjectModelIngestionUpdatedMessageType.updated);
|
||||||
|
subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||||
|
public async Task ProjectModelIngestionUpdate_CancelSubscriptionIsNotCalled()
|
||||||
|
{
|
||||||
|
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||||
|
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||||
|
);
|
||||||
|
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||||
|
|
||||||
|
using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.id);
|
||||||
|
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||||
|
|
||||||
|
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||||
|
|
||||||
|
await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "this shouldn't cancel", null));
|
||||||
|
|
||||||
|
await Task.Delay(WAIT_PERIOD); // Give time to subscription to maybe fire
|
||||||
|
|
||||||
|
tcs.Task.IsCompleted.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,17 +21,19 @@ public class WorkspaceResourceTests
|
|||||||
return testUser;
|
return testUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact, Trait("Server", "Internal")]
|
||||||
public async Task TestGetWorkspace()
|
public async Task TestGetWorkspace()
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
|
||||||
await Verify(ex);
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestGetProjects()
|
public async Task TestGetProjects()
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
|
||||||
await Verify(ex);
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
|||||||
|
|
||||||
[assembly: AssemblyTrait("Category", "Integration")]
|
[assembly: AssemblyTrait("Category", "Integration")]
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
[assembly: CollectionBehavior(MaxParallelThreads = 8)]
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Integration;
|
namespace Speckle.Sdk.Tests.Integration;
|
||||||
|
|
||||||
public static class Fixtures
|
public static class Fixtures
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Reflection;
|
using FluentAssertions;
|
||||||
using FluentAssertions;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Speckle.Sdk.Api;
|
using Speckle.Sdk.Api;
|
||||||
using Speckle.Sdk.Host;
|
|
||||||
using Speckle.Sdk.Models;
|
using Speckle.Sdk.Models;
|
||||||
using Speckle.Sdk.Transports;
|
using Speckle.Sdk.Transports;
|
||||||
|
|
||||||
@@ -16,8 +14,7 @@ public class MemoryTransportTests : IDisposable
|
|||||||
public MemoryTransportTests()
|
public MemoryTransportTests()
|
||||||
{
|
{
|
||||||
CleanData();
|
CleanData();
|
||||||
TypeLoader.Reset();
|
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_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 Microsoft.Extensions.DependencyInjection;
|
||||||
using Speckle.Sdk.Api;
|
using Speckle.Sdk.Api;
|
||||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||||
using Speckle.Sdk.Api.GraphQL.Models;
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
using Speckle.Sdk.Host;
|
|
||||||
using Speckle.Sdk.Models;
|
|
||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Integration;
|
namespace Speckle.Sdk.Tests.Integration;
|
||||||
|
|
||||||
@@ -19,8 +16,6 @@ public sealed class SendReceiveTests : IAsyncLifetime
|
|||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||||
ClearCache();
|
ClearCache();
|
||||||
|
|||||||
@@ -16,4 +16,7 @@
|
|||||||
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||||
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Pipelines\Send\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.Bcl.AsyncInterfaces": {
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[5.0.0, )",
|
"requested": "[9.0.4, )",
|
||||||
"resolved": "1.1.0",
|
"resolved": "1.1.0",
|
||||||
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ using Speckle.Sdk.Transports;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public class Closures
|
public class Closures
|
||||||
{
|
{
|
||||||
private readonly IOperations _operations;
|
private readonly IOperations _operations;
|
||||||
|
|
||||||
public Closures()
|
public Closures()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Speckle.Sdk.Transports;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public sealed partial class OperationsReceiveTests : IDisposable
|
public sealed partial class OperationsReceiveTests : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Base[] s_testObjects;
|
private static readonly Base[] s_testObjects;
|
||||||
@@ -44,8 +45,7 @@ public sealed partial class OperationsReceiveTests : IDisposable
|
|||||||
|
|
||||||
private static void Reset()
|
private static void Reset()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> TestCases()
|
public static IEnumerable<object[]> TestCases()
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ using Speckle.Sdk.Transports;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public class SendObjectReferences
|
public class SendObjectReferences
|
||||||
{
|
{
|
||||||
private readonly IOperations _operations;
|
private readonly IOperations _operations;
|
||||||
|
|
||||||
public SendObjectReferences()
|
public SendObjectReferences()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ using Speckle.Sdk.Transports;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public sealed class SendReceiveLocal : IDisposable
|
public sealed class SendReceiveLocal : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IOperations _operations;
|
private readonly IOperations _operations;
|
||||||
|
|
||||||
public SendReceiveLocal()
|
public SendReceiveLocal()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ using Point = Speckle.Sdk.Tests.Unit.Host.Point;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public class ObjectSerialization
|
public class ObjectSerialization
|
||||||
{
|
{
|
||||||
private readonly IOperations _operations;
|
private readonly IOperations _operations;
|
||||||
|
|
||||||
public ObjectSerialization()
|
public ObjectSerialization()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
|
||||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
_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;
|
||||||
@@ -9,6 +9,7 @@ using Speckle.Sdk.Testing;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||||
public sealed class AccountManagerTests : MoqTest
|
public sealed class AccountManagerTests : MoqTest
|
||||||
{
|
{
|
||||||
private class TestAccountFactory : IAccountFactory
|
private class TestAccountFactory : IAccountFactory
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||||
public class AccountServerMigrationTests : IDisposable
|
public class AccountServerMigrationTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly List<Account> _accountsToCleanUp = [];
|
private readonly List<Account> _accountsToCleanUp = [];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||||
public class CredentialInfrastructure : IDisposable
|
public class CredentialInfrastructure : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IAccountManager _accountManager;
|
private readonly IAccountManager _accountManager;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ using Speckle.Sdk.Models;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Models;
|
namespace Speckle.Sdk.Tests.Unit.Models;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public class BaseTests
|
public class BaseTests
|
||||||
{
|
{
|
||||||
public BaseTests()
|
public BaseTests()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ using Speckle.Sdk.Models;
|
|||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Models;
|
namespace Speckle.Sdk.Tests.Unit.Models;
|
||||||
|
|
||||||
|
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||||
public class DynamicBaseTests
|
public class DynamicBaseTests
|
||||||
{
|
{
|
||||||
public DynamicBaseTests()
|
public DynamicBaseTests()
|
||||||
{
|
{
|
||||||
TypeLoader.Reset();
|
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user