Compare commits

...

2 Commits

Author SHA1 Message Date
Jedd Morgan 3fbd9c17ba format
.NET Build and Publish / build (push) Has been cancelled
2025-11-24 18:41:10 +00:00
Jedd Morgan 937eb94730 First pass 2025-11-24 18:40:17 +00:00
6 changed files with 420 additions and 0 deletions
+2
View File
@@ -35,6 +35,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
public WorkspaceResource Workspace { get; }
public ServerResource Server { get; }
public FileImportResource FileImport { get; }
public IngestResource Ingest { get; }
public Uri ServerUrl => new(Account.serverInfo.url);
@@ -71,6 +72,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
Workspace = new(this);
Server = new(this);
FileImport = new(this, blobApiFactory.Create(account));
Ingest = new(this);
}
[AutoInterfaceIgnore]
@@ -0,0 +1,19 @@
namespace Speckle.Sdk.Api.GraphQL.Inputs;
public record IngestCreateInput(
string fileName,
int? maxIdleTimeoutMinutes,
string modelId,
string projectId,
string sourceApplication,
string sourceApplicationVersion,
IReadOnlyDictionary<string, object?> sourceFileData
);
public record IngestFinishInput(string id, string? message, string objectId, string projectId);
public record IngestErrorInput(string errorReason, string errorStacktrace, string id, string projectId);
public record CancelRequestInput(string id, string projectId);
public record IngestUpdateInput(string id, double? progress, string? progressMessage, string projectId);
@@ -0,0 +1,23 @@
namespace Speckle.Sdk.Api.GraphQL.Models;
public sealed class Ingest
{
public required DateTime createdAt { get; init; }
public required string errorReason { get; init; }
public required string errorStacktrace { get; init; }
public required string fileName { get; init; }
public required string id { get; init; }
public required long maxIdleTimeoutMinutes { get; init; }
public required string modelId { get; init; }
public required Dictionary<string, object?> performanceData { get; init; }
public required double progress { get; init; }
public required string? progressMessage { get; init; }
public required string projectId { get; init; }
public required string sourceApplication { get; init; }
public required string sourceApplicationVersion { get; init; }
public required Dictionary<string, object?> sourceFileData { get; init; }
public required string status { get; init; }
public required DateTime updatedAt { get; init; }
public required string versionId { get; init; }
public required LimitedUser user { get; init; }
}
@@ -0,0 +1,253 @@
using GraphQL;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Models.Responses;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
namespace Speckle.Sdk.Api.GraphQL.Resources;
public sealed class IngestResource
{
private readonly ISpeckleGraphQLClient _client;
internal IngestResource(ISpeckleGraphQLClient client)
{
_client = client;
}
/// <param name="modelId"></param>
/// <param name="projectId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ResourceCollection<Ingest>> GetIngests(
string modelId,
string projectId,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
query GetIngest($modelId: String!, $projectId: String!) {
data:project(id: $projectId) {
data:model(id: $modelId) {
data:ingests {
cursor
items {
createdAt
errorReason
errorStacktrace
fileName
id
maxIdleTimeoutMinutes
modelId
performanceData
progress
progressMessage
projectId
sourceApplication
sourceApplicationVersion
sourceFileData
status
updatedAt
versionId
user {
avatar
bio
company
id
name
role
verified
}
}
}
}
}
}
""";
var request = new GraphQLRequest { Query = QUERY, Variables = new { modelId, projectId } };
var response = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ResourceCollection<Ingest>>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return response.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<bool> Update(IngestUpdateInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestUpdate($projectId: ID!, $input: IngestUpdateInput!) {
data: projectMutations {
data: ingestMutations(projectId: $projectId) {
data: update(input: $input)
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input, input.projectId } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<bool>>>>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<Ingest> Create(IngestCreateInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestCreate($projectId: ID!, $input: IngestCreateInput!) {
data: projectMutations {
data:ingestMutations(projectId: $projectId) {
data:create(input: $input) {
createdAt
errorReason
errorStacktrace
fileName
id
maxIdleTimeoutMinutes
modelId
performanceData
progress
progressMessage
projectId
sourceApplication
sourceApplicationVersion
sourceFileData
status
updatedAt
versionId
user {
avatar
bio
company
id
name
role
verified
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input, input.projectId } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<Ingest>>>>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<Version> End(IngestFinishInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestEnd($projectId: ID!, $input: IngestFinishInput!) {
data: projectMutations {
data:ingestMutations(projectId: $projectId) {
data:end(input: $input) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input, input.projectId } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<Version>>>>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<bool> Error(IngestErrorInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestError($projectId: ID!, $input: IngestErrorInput!) {
data: projectMutations {
data:ingestMutations(projectId: $projectId) {
data:error(input: $input)
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input, input.projectId } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<bool>>>>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<bool> Cancel(CancelRequestInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestCancel($projectId: ID!, $input: CancelRequestInput!) {
data:projectMutations {
data:ingestMutations(projectId: $projectId) {
data:cancel(input: $input)
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input, input.projectId } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<bool>>>>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data;
}
}
@@ -0,0 +1,122 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Resources;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public class IngestResourceTests : IAsyncLifetime
{
private IClient _testUser;
private IngestResource Sut => _testUser.Ingest;
private Project _project;
private Model _model;
private IOperations _operations;
public Task DisposeAsync() => Task.CompletedTask;
public async Task InitializeAsync()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
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 input = new IngestCreateInput(
"myTestFile",
1,
_model.id,
_project.id,
".NET",
"0.0.0",
new Dictionary<string, object?>()
);
Ingest ingest = await Sut.Create(input);
var errorInput = new IngestErrorInput("A bad thing happened", "Over hear!", ingest.id, _project.id);
var res = await Sut.Error(errorInput);
Assert.True(res);
var result = await Sut.GetIngests(_model.id, _project.id);
await Verify(result);
}
[Fact]
public async Task CreateAndCancel()
{
var input = new IngestCreateInput(
"myTestFile",
1,
_model.id,
_project.id,
".NET",
"0.0.0",
new Dictionary<string, object?>()
);
Ingest ingest = await Sut.Create(input);
var errorInput = new CancelRequestInput(ingest.id, _project.id);
var res = await Sut.Cancel(errorInput);
Assert.True(res);
var result = await Sut.GetIngests(_model.id, _project.id);
await Verify(result);
}
[Fact]
public async Task CreateAndEnd()
{
var create = new IngestCreateInput(
"myTestFile",
1,
_model.id,
_project.id,
".NET",
"0.0.0",
new Dictionary<string, object?>()
);
Ingest ingest = await Sut.Create(create);
var myObject = Fixtures.GenerateNestedObject();
var sendResult = await _operations.Send2(
_testUser.ServerUrl,
_project.id,
_testUser.Account.token,
myObject,
new Progress<ProgressArgs>(x =>
{
var updateInput = new IngestUpdateInput(
ingest.id,
x.Total == null ? null : x.Count / x.Total,
$"{x.Count} / {x.Total}",
_project.id
);
_ = Sut.Update(updateInput).Result;
}),
CancellationToken.None,
new(true, true)
);
var finish = new IngestFinishInput(ingest.id, "Yay! we completed", sendResult.RootId, _project.id);
var res = await Sut.End(finish);
Assert.NotNull(res);
var result = await Sut.GetIngests(_model.id, _project.id);
await Verify(result);
}
}