From 0b010912092daa98a2e83db1848dd05405f69190 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Wed, 22 Oct 2025 10:53:38 +0100 Subject: [PATCH 01/17] feat(objects): Adds new camera and view proxy classes (#407) * Adds new camera and view proxy classes * Update ViewProxy.cs --- src/Speckle.Objects/Other/Camera.cs | 32 ++++++++++++++++++++++++++ src/Speckle.Objects/Other/ViewProxy.cs | 27 ++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/Speckle.Objects/Other/Camera.cs create mode 100644 src/Speckle.Objects/Other/ViewProxy.cs diff --git a/src/Speckle.Objects/Other/Camera.cs b/src/Speckle.Objects/Other/Camera.cs new file mode 100644 index 00000000..fd63e823 --- /dev/null +++ b/src/Speckle.Objects/Other/Camera.cs @@ -0,0 +1,32 @@ +using Speckle.Objects.Geometry; +using Speckle.Sdk.Models; + +namespace Speckle.Objects.Other; + +/// +/// Camera class to represent a camera for a 3D view. +/// +/// Assumes a Z-up, right-handed convention for orientation vectors +[SpeckleType("Objects.Other.Camera")] +public class Camera : Base +{ + /// + /// The location of the camera + /// + public required Point position { get; set; } + + /// + /// The unit up vector of the camera + /// + public required Vector up { get; set; } + + /// + /// The unit forward vector of the camera + /// + public required Vector forward { get; set; } + + /// + /// Indicates whether or not the camera is using orthographic projection. By default, the projection should be perspective. + /// + public bool isOrthographic { get; set; } +} diff --git a/src/Speckle.Objects/Other/ViewProxy.cs b/src/Speckle.Objects/Other/ViewProxy.cs new file mode 100644 index 00000000..aeac1027 --- /dev/null +++ b/src/Speckle.Objects/Other/ViewProxy.cs @@ -0,0 +1,27 @@ +using Speckle.Sdk.Models; +using Speckle.Sdk.Models.Proxies; + +namespace Speckle.Objects.Other; + +/// +/// Proxy for 3D views. +/// +/// The list points to the applicationIds of any atomic objects that are visible in this view. An empty objects list indicates that all objects by default are visible. +[SpeckleType("Objects.Other.ViewProxy")] +public class ViewProxy : Base, IProxyCollection +{ + /// + /// The list of application ids of objects that belong to this view + /// + public required List objects { get; set; } + + /// + /// The camera used for this view + /// + public required Camera value { get; set; } + + /// + /// The name of this view + /// + public required string name { get; set; } +} From c2735f0a32629092c57089a4be32c66c35bae39c Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Fri, 24 Oct 2025 11:46:30 +0100 Subject: [PATCH 02/17] chore(objects): update view proxy and camera classes (#408) * Adds new camera and view proxy classes * Update ViewProxy.cs * removes viewproxy and orthographic prop on camera class --- src/Speckle.Objects/Other/Camera.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Speckle.Objects/Other/Camera.cs b/src/Speckle.Objects/Other/Camera.cs index fd63e823..c48a708e 100644 --- a/src/Speckle.Objects/Other/Camera.cs +++ b/src/Speckle.Objects/Other/Camera.cs @@ -4,12 +4,17 @@ using Speckle.Sdk.Models; namespace Speckle.Objects.Other; /// -/// Camera class to represent a camera for a 3D view. +/// Camera class to represent a perspective camera for a 3D view. /// /// Assumes a Z-up, right-handed convention for orientation vectors [SpeckleType("Objects.Other.Camera")] public class Camera : Base { + /// + /// The name of the view that is created by this camera + /// + public required string name { get; set; } + /// /// The location of the camera /// @@ -24,9 +29,4 @@ public class Camera : Base /// The unit forward vector of the camera /// public required Vector forward { get; set; } - - /// - /// Indicates whether or not the camera is using orthographic projection. By default, the projection should be perspective. - /// - public bool isOrthographic { get; set; } } From f1a64590d7780068e54de50e367e959e5ed0cc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Sat, 25 Oct 2025 11:59:39 +0200 Subject: [PATCH 03/17] chore(models): adds RootCollection --- .../Models/Collections/RootCollection.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Speckle.Sdk/Models/Collections/RootCollection.cs diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs new file mode 100644 index 00000000..6c9d05d8 --- /dev/null +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -0,0 +1,27 @@ +namespace Speckle.Sdk.Models.Collections; + +/// +/// Root collection that represents the top-level commit object. +/// Extends Collection to include model-wide properties that apply to the entire model. +/// +[SpeckleType("Speckle.Core.Models.Collections.RootCollection")] +public class RootCollection : Collection +{ + /// + /// Constructor for a root collection. + /// + /// The human-readable name of this root collection + public RootCollection(string name) : base(name) { } + + /// + /// Model-wide properties that apply to the entire model. + /// + /// + /// These are intended for model-level metadata such as total area, project information, or analysis results. + /// + public Dictionary? properties + { + get => this["properties"] as Dictionary; + set => this["properties"] = value; + } +} From c3230d5d91d21fcd00db83b327e24267ed33eddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Sat, 25 Oct 2025 12:09:34 +0200 Subject: [PATCH 04/17] refactor: naming conflict --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index 6c9d05d8..d9890721 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -19,7 +19,7 @@ public class RootCollection : Collection /// /// These are intended for model-level metadata such as total area, project information, or analysis results. /// - public Dictionary? properties + public Dictionary? rootProperties { get => this["properties"] as Dictionary; set => this["properties"] = value; From 9a879fd1ac0050368eb433fac165fb6b02a68a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Sat, 25 Oct 2025 13:35:58 +0200 Subject: [PATCH 05/17] fix: parameterless constructor --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index d9890721..b89cc7b7 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -7,6 +7,11 @@ namespace Speckle.Sdk.Models.Collections; [SpeckleType("Speckle.Core.Models.Collections.RootCollection")] public class RootCollection : Collection { + /// + /// Parameterless constructor for deserialization. + /// + public RootCollection() { } + /// /// Constructor for a root collection. /// From 3f49bb05d13cfef1e471600efbe82e7e9e7c4b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Sat, 25 Oct 2025 15:20:24 +0200 Subject: [PATCH 06/17] chore: cleanup --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index b89cc7b7..6e546fff 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -24,9 +24,5 @@ public class RootCollection : Collection /// /// These are intended for model-level metadata such as total area, project information, or analysis results. /// - public Dictionary? rootProperties - { - get => this["properties"] as Dictionary; - set => this["properties"] = value; - } + public Dictionary? rootProperties { get; set; } } From 63f06c8541b2019ffb88d11b761308df1a728c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Sat, 25 Oct 2025 15:41:00 +0200 Subject: [PATCH 07/17] chore: format --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index 6e546fff..17f61d7a 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -11,12 +11,13 @@ public class RootCollection : Collection /// Parameterless constructor for deserialization. /// public RootCollection() { } - + /// /// Constructor for a root collection. /// /// The human-readable name of this root collection - public RootCollection(string name) : base(name) { } + public RootCollection(string name) + : base(name) { } /// /// Model-wide properties that apply to the entire model. From b67eb8d8af1e689e8560f06d75952cdedce6f5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Mon, 27 Oct 2025 13:03:52 +0200 Subject: [PATCH 08/17] refactor: model properties to properties --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index 17f61d7a..ace5979c 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -7,15 +7,8 @@ namespace Speckle.Sdk.Models.Collections; [SpeckleType("Speckle.Core.Models.Collections.RootCollection")] public class RootCollection : Collection { - /// - /// Parameterless constructor for deserialization. - /// public RootCollection() { } - /// - /// Constructor for a root collection. - /// - /// The human-readable name of this root collection public RootCollection(string name) : base(name) { } @@ -25,5 +18,5 @@ public class RootCollection : Collection /// /// These are intended for model-level metadata such as total area, project information, or analysis results. /// - public Dictionary? rootProperties { get; set; } + public Dictionary? properties { get; set; } } From 6e35d6af6d9f96b641e9ff5dfae9ca652c3e6fd6 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:33:47 +0000 Subject: [PATCH 09/17] Unexpose attribute mask --- Directory.Build.targets | 2 +- .../V2/Receive/DeserializeProcess.cs | 4 +--- .../Serialisation/V2/Receive/ObjectLoader.cs | 20 +++++++++---------- .../ExceptionTests.cs | 11 ++++------ .../SerializationTests.cs | 3 +-- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index ddff399d..861d0313 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -2,7 +2,7 @@ - CS0618;CA1034;CA2201;CA1051;CA1040;CA1724; + CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065; IDE0044;IDE0130;CA1508; CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831; diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs index 3d3cefad..b0e65a7c 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs @@ -14,8 +14,7 @@ public record DeserializeProcessOptions( bool ThrowOnMissingReferences = true, bool SkipInvalidConverts = false, int? MaxParallelism = null, - bool SkipServer = false, - string? AttributeMask = null + bool SkipServer = false ); public partial interface IDeserializeProcess : IAsyncDisposable; @@ -45,7 +44,6 @@ public sealed class DeserializeProcess( new ObjectLoader( sqLiteJsonCacheManager, serverObjectManager, - options?.AttributeMask, progress, loggerFactory.CreateLogger(), cancellationToken diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs index c3e37fa2..c58ef95d 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs @@ -16,13 +16,10 @@ public partial interface IObjectLoader : IDisposable; public sealed class ObjectLoader( ISqLiteJsonCacheManager sqLiteJsonCacheManager, IServerObjectManager serverObjectManager, - string? attributeMask, IProgress? progress, ILogger logger, CancellationToken cancellationToken -#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. ) : ChannelLoader(cancellationToken), IObjectLoader -#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. { private int? _allChildrenCount; private long _checkCache; @@ -30,6 +27,7 @@ public sealed class ObjectLoader( private long _downloaded; private long _totalToDownload; private DeserializeProcessOptions _options = new(); + private readonly CancellationToken _cancellationToken = cancellationToken; [AutoInterfaceIgnore] public void Dispose() => sqLiteJsonCacheManager.Dispose(); @@ -47,7 +45,7 @@ public sealed class ObjectLoader( { //assume everything exists as the root is there. var allChildren = ClosureParser - .GetClosuresSorted(rootJson, cancellationToken) + .GetClosuresSorted(rootJson, _cancellationToken) .Select(x => new Id(x.Item1)) .ToList(); //this probably yields away from the Main thread to let host apps update progress @@ -60,11 +58,11 @@ public sealed class ObjectLoader( if (!options.SkipServer) { rootJson = await serverObjectManager - .DownloadSingleObject(rootId, progress, cancellationToken) + .DownloadSingleObject(rootId, progress, _cancellationToken) .NotNull() .ConfigureAwait(false); IReadOnlyCollection allChildrenIds = ClosureParser - .GetClosures(rootJson, cancellationToken) + .GetClosures(rootJson, _cancellationToken) .OrderByDescending(x => x.Item2) .Select(x => new Id(x.Item1)) .Where(x => !x.Value.StartsWith("blob", StringComparison.Ordinal)) @@ -112,13 +110,13 @@ public sealed class ObjectLoader( await foreach ( var (id, json) in serverObjectManager.DownloadObjects( ids.Select(x => x.NotNull()).ToList(), - attributeMask, + null, //TODO: Implement attribute masking in a safe way that will not poison SQLite DB. progress, - cancellationToken + _cancellationToken ) ) { - cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); Interlocked.Increment(ref _downloaded); progress?.Report(new(ProgressEvent.DownloadObjects, _downloaded, _totalToDownload)); toCache.Add(new(new(id), new(json), true, null)); @@ -140,7 +138,7 @@ public sealed class ObjectLoader( { if (!_options.SkipCache) { - cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value))); Interlocked.Exchange(ref _cached, _cached + batch.Count); progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _allChildrenCount)); @@ -170,7 +168,7 @@ public sealed class ObjectLoader( private void ThrowIfFailed() { //always check for cancellation first - cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); if (Exception is not null) { throw new SpeckleException($"Error while loading: {Exception.Message}", Exception); diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs index 7f73bf60..bc71c3d2 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs @@ -112,16 +112,15 @@ public class ExceptionTests new DummySqLiteReceiveManager(new Dictionary()), new ExceptionServerObjectManager(), null, - null, new NullLogger(), - default + CancellationToken.None ); await using var process = new DeserializeProcess( o, null, new BaseDeserializer(new ObjectDeserializerFactory()), new NullLoggerFactory(), - default, + CancellationToken.None, new(SkipCache: true, MaxParallelism: 1, SkipServer: true) ); @@ -145,7 +144,7 @@ public class ExceptionTests null, new BaseDeserializer(new ObjectDeserializerFactory()), new NullLoggerFactory(), - default, + CancellationToken.None, new(true, MaxParallelism: 1) ); @@ -170,7 +169,7 @@ public class ExceptionTests null, new BaseDeserializer(new ObjectDeserializerFactory()), new NullLoggerFactory(), - default, + CancellationToken.None, new(MaxParallelism: 1) ); @@ -195,9 +194,7 @@ public class ExceptionTests [SpeckleType("Objects.Geometry.BadBase")] public class BadBase : Base { -#pragma warning disable CA1065 public string BadProp => throw new NotImplementedException(); -#pragma warning restore CA1065 } [Fact] diff --git a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs index 82bc9bee..e7f5f0e7 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs @@ -202,9 +202,8 @@ public class SerializationTests new DummySqLiteReceiveManager(closures), new DummyReceiveServerObjectManager(closures), null, - null, new NullLogger(), - default + CancellationToken.None ) ) { From 8785e49f7378f8bd9b55549932141937555bc2eb Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:12:12 +0000 Subject: [PATCH 10/17] Add send/receive integration tests (#412) --- src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs | 2 +- .../Api/Operations/Operations.Receive.cs | 1 + .../V2/Receive/DeserializeProcess.cs | 2 +- ...ceiveNonExistentObjectThrows.verified.json | 6 + ...eiveNonExistentProjectThrows.verified.json | 6 + ...dReceiveTests.SendAndReceive.verified.json | 4 + ...ReceiveTests.SendInvalidData.verified.json | 18 ++ .../SendReceiveTests.cs | 190 ++++++++++++++++++ 8 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs diff --git a/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs b/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs index 01da6610..0da48353 100644 --- a/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs +++ b/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs @@ -14,7 +14,7 @@ public sealed class Comment public string rawText { get; init; } public ResourceCollection replies { get; init; } public CommentReplyAuthorCollection replyAuthors { get; init; } - public List resources { get; init; } + public List resources { get; init; } //todo: add resourceIds/baseResourceIds public string? screenshot { get; init; } public DateTime updatedAt { get; init; } public DateTime? viewedAt { get; init; } diff --git a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs index 4aa4e00a..1c946b8f 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs @@ -16,6 +16,7 @@ public partial class Operations /// No transports were specified /// The was /// Serialization or Send operation was unsuccessful + /// HTTP layer errors /// The requested cancellation public async Task Receive2( Uri url, diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs index 3d3cefad..dd486f20 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs @@ -10,7 +10,7 @@ using Speckle.Sdk.Transports; namespace Speckle.Sdk.Serialisation.V2.Receive; public record DeserializeProcessOptions( - bool SkipCache = false, + bool SkipCache = false, //TODO: This appears to be bugged when set to `true`, `LoadId` depends on sqlite bool ThrowOnMissingReferences = true, bool SkipInvalidConverts = false, int? MaxParallelism = null, diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json new file mode 100644 index 00000000..c48b611b --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json @@ -0,0 +1,6 @@ +{ + "Data": {}, + "Message": "Response status code does not indicate success: 404 (Not Found).", + "StatusCode": "NotFound", + "Type": "HttpRequestException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json new file mode 100644 index 00000000..c48b611b --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json @@ -0,0 +1,6 @@ +{ + "Data": {}, + "Message": "Response status code does not indicate success: 404 (Not Found).", + "StatusCode": "NotFound", + "Type": "HttpRequestException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json new file mode 100644 index 00000000..a931619e --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json @@ -0,0 +1,4 @@ +{ + "ConvertedReferences": {}, + "RootId": "5313a8f61e1fa7abe9bf716ddfc767bd" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json new file mode 100644 index 00000000..ce2a6547 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json @@ -0,0 +1,18 @@ +{ + "Data": {}, + "InnerException": { + "$type": "SpeckleSerializeException", + "Data": {}, + "InnerException": { + "$type": "ArgumentException", + "Data": {}, + "Message": "Unsupported value in serialization: System.Text.StringBuilder", + "ParamName": "obj", + "Type": "ArgumentException" + }, + "Message": "Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base", + "Type": "SpeckleSerializeException" + }, + "Message": "Error while sending: Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base", + "Type": "SpeckleException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs new file mode 100644 index 00000000..78227b1a --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs @@ -0,0 +1,190 @@ +using System.Reflection; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Speckle.Sdk.Api; +using Speckle.Sdk.Api.GraphQL.Enums; +using Speckle.Sdk.Api.GraphQL.Models; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; + +namespace Speckle.Sdk.Tests.Integration; + +public sealed class SendReceiveTests : IAsyncLifetime +{ + private Project _project; + private IClient _client; + private IOperations _operations; + private const string NON_EXISTENT_OBJECT_ID = "0a480dfb7aa774f19a82bee9d6320abd"; + private const string NON_EXISTENT_PROJECT_ID = "8cdc651d13"; + + public async Task InitializeAsync() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly()); + var serviceProvider = TestServiceSetup.GetServiceProvider(); + _operations = serviceProvider.GetRequiredService(); + ClearCache(); + + _client = await Fixtures.SeedUserWithClient(); + _project = await _client.Project.Create(new("Blobber", "Flobber", ProjectVisibility.Private)); + } + + [Fact] + public async Task SendAndReceive() + { + var myObject = Fixtures.GenerateNestedObject(); + string expectedId = myObject.GetId(true); + + //SEND + var fistSend = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + myObject, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, fistSend.RootId); + await Verify(fistSend); + + //RECEIVE + var received = await _operations.Receive2( + _client.ServerUrl, + _project.id, + fistSend.RootId, + _client.Account.token, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, received.id); + + //SEND AGAIN! + var secondSend = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + received, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, secondSend.RootId); + + //RECEIVE AGAIN, but using cache + ClearCache(); + var secondReceive = await _operations.Receive2( + _client.ServerUrl, + _project.id, + fistSend.RootId, + _client.Account.token, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, secondReceive.id); + } + + private void ClearCache() { } + + [Fact] + public async Task ReceiveNonExistentObjectThrows() + { + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task ReceiveNonExistentProjectThrows() + { + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + NON_EXISTENT_PROJECT_ID, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task SendInvalidData() + { + var myObject = Fixtures.GenerateNestedObject(); + myObject["invalidProp"] = new StringBuilder(); //Serializer does not support serializing this type + + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + myObject, + null, + CancellationToken.None, + new(SkipCacheRead: true, SkipCacheWrite: true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task ReceiveNonAuthThrows() + { + using IClient unauthed = Fixtures.Unauthed; + await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + unauthed.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + unauthed.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + } + + [Fact] + public async Task ReceiveCancellation() + { + using CancellationTokenSource ct = new(); + await ct.CancelAsync(); + await Assert.ThrowsAnyAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + ct.Token, + new(true) + ); + }); + } + + public Task DisposeAsync() + { + _client?.Dispose(); + return Task.CompletedTask; + } +} From c3f944dcf13f4cd8b76df3f7500b683e72f497b8 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 28 Oct 2025 13:52:19 +0000 Subject: [PATCH 11/17] Delete ViewProxy.cs (#415) --- src/Speckle.Objects/Other/ViewProxy.cs | 27 -------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/Speckle.Objects/Other/ViewProxy.cs diff --git a/src/Speckle.Objects/Other/ViewProxy.cs b/src/Speckle.Objects/Other/ViewProxy.cs deleted file mode 100644 index aeac1027..00000000 --- a/src/Speckle.Objects/Other/ViewProxy.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Speckle.Sdk.Models; -using Speckle.Sdk.Models.Proxies; - -namespace Speckle.Objects.Other; - -/// -/// Proxy for 3D views. -/// -/// The list points to the applicationIds of any atomic objects that are visible in this view. An empty objects list indicates that all objects by default are visible. -[SpeckleType("Objects.Other.ViewProxy")] -public class ViewProxy : Base, IProxyCollection -{ - /// - /// The list of application ids of objects that belong to this view - /// - public required List objects { get; set; } - - /// - /// The camera used for this view - /// - public required Camera value { get; set; } - - /// - /// The name of this view - /// - public required string name { get; set; } -} From 07713b41e110b004419874497d92685cb577784a Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:48:37 +0000 Subject: [PATCH 12/17] Fix(gql)!: Treat UNAUTHORIZED_ACCESS_ERROR as an `SpeckleGraphQLForbiddenException` (#411) * Respect UNAUTHORIZED_ACCESS_ERROR * Correct test setup for automate * dumb dumb typo --- src/Speckle.Sdk/Api/Exceptions.cs | 3 +- .../Api/GraphQL/GraphQLErrorHandler.cs | 4 +- .../Speckle.Automate.Sdk.Integration.csproj | 7 ++ .../SpeckleAutomate.cs | 6 +- .../packages.lock.json | 68 +++++++++---------- .../ProjectResourceExceptionalTests.cs | 2 +- .../Api/GraphQLErrorHandler.cs | 1 + 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/Speckle.Sdk/Api/Exceptions.cs b/src/Speckle.Sdk/Api/Exceptions.cs index 94833754..0e0de06c 100644 --- a/src/Speckle.Sdk/Api/Exceptions.cs +++ b/src/Speckle.Sdk/Api/Exceptions.cs @@ -23,9 +23,10 @@ public class SpeckleGraphQLException : SpeckleException } /// -/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception. +/// Represents a "FORBIDDEN" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception. /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden +/// https://github.com/specklesystems/speckle-server/blob/v2.23.18/packages/server/modules/shared/errors/index.ts#L34 /// public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException { diff --git a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs index 14e902e6..a2e4b54f 100644 --- a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs +++ b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs @@ -28,7 +28,9 @@ internal static class GraphQLErrorHandler var ex = code switch { "GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message), - "FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message), + "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED_ACCESS_ERROR" => new SpeckleGraphQLForbiddenException( + message + ), "STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message), "BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message), "INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message), diff --git a/tests/Speckle.Automate.Sdk.Integration/Speckle.Automate.Sdk.Integration.csproj b/tests/Speckle.Automate.Sdk.Integration/Speckle.Automate.Sdk.Integration.csproj index 4ce75cd2..4bdacd22 100644 --- a/tests/Speckle.Automate.Sdk.Integration/Speckle.Automate.Sdk.Integration.csproj +++ b/tests/Speckle.Automate.Sdk.Integration/Speckle.Automate.Sdk.Integration.csproj @@ -3,6 +3,13 @@ net8.0 true + + + + + + + diff --git a/tests/Speckle.Automate.Sdk.Integration/SpeckleAutomate.cs b/tests/Speckle.Automate.Sdk.Integration/SpeckleAutomate.cs index c9b5336d..0ac3a1f9 100644 --- a/tests/Speckle.Automate.Sdk.Integration/SpeckleAutomate.cs +++ b/tests/Speckle.Automate.Sdk.Integration/SpeckleAutomate.cs @@ -25,7 +25,9 @@ public sealed class AutomationContextTest : IAsyncLifetime public async Task InitializeAsync() { - var serviceProvider = TestServiceSetup.GetServiceProvider(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddAutomateSdk(); + var serviceProvider = serviceCollection.BuildServiceProvider(); _account = await Fixtures.SeedUser().ConfigureAwait(false); _client = serviceProvider.GetRequiredService().Create(_account); _runner = serviceProvider.GetRequiredService(); @@ -42,7 +44,7 @@ public sealed class AutomationContextTest : IAsyncLifetime private async Task AutomationRunData(Base testObject) { Project project = await _client.Project.Create(new("Automate function e2e test", null, ProjectVisibility.Public)); - const string BRANCH_NAME = "main"; + const string BRANCH_NAME = "Trigger"; var model = await _client.Model.Create(new(BRANCH_NAME, null, project.id)); string modelId = model.id; diff --git a/tests/Speckle.Automate.Sdk.Integration/packages.lock.json b/tests/Speckle.Automate.Sdk.Integration/packages.lock.json index c5675743..78c3841b 100644 --- a/tests/Speckle.Automate.Sdk.Integration/packages.lock.json +++ b/tests/Speckle.Automate.Sdk.Integration/packages.lock.json @@ -2,6 +2,28 @@ "version": 2, "dependencies": { "net8.0": { + "altcover": { + "type": "Direct", + "requested": "[9.0.1, )", + "resolved": "9.0.1", + "contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA==" + }, + "AwesomeAssertions": { + "type": "Direct", + "requested": "[8.1.0, )", + "resolved": "8.1.0", + "contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.13.0, )", + "resolved": "17.13.0", + "contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==", + "dependencies": { + "Microsoft.CodeCoverage": "17.13.0", + "Microsoft.TestPlatform.TestHost": "17.13.0" + } + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -24,6 +46,18 @@ "resolved": "0.9.6", "contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w==" }, + "xunit.assert": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.0.2, )", + "resolved": "3.0.2", + "contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow==" + }, "Argon": { "type": "Transitive", "resolved": "0.28.0", @@ -364,18 +398,6 @@ "xunit.runner.visualstudio": "[3.0.2, )" } }, - "altcover": { - "type": "CentralTransitive", - "requested": "[9.0.1, )", - "resolved": "9.0.1", - "contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA==" - }, - "AwesomeAssertions": { - "type": "CentralTransitive", - "requested": "[8.1.0, )", - "resolved": "8.1.0", - "contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw==" - }, "GraphQL.Client": { "type": "CentralTransitive", "requested": "[6.0.0, )", @@ -424,16 +446,6 @@ "Microsoft.Extensions.Options": "2.2.0" } }, - "Microsoft.NET.Test.Sdk": { - "type": "CentralTransitive", - "requested": "[17.13.0, )", - "resolved": "17.13.0", - "contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==", - "dependencies": { - "Microsoft.CodeCoverage": "17.13.0", - "Microsoft.TestPlatform.TestHost": "17.13.0" - } - }, "Moq": { "type": "CentralTransitive", "requested": "[4.20.72, )", @@ -515,18 +527,6 @@ "xunit.assert": "2.9.3", "xunit.core": "[2.9.3]" } - }, - "xunit.assert": { - "type": "CentralTransitive", - "requested": "[2.9.3, )", - "resolved": "2.9.3", - "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" - }, - "xunit.runner.visualstudio": { - "type": "CentralTransitive", - "requested": "[3.0.2, )", - "resolved": "3.0.2", - "contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow==" } } } diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs index 7da9f67e..94034402 100644 --- a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs +++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs @@ -93,7 +93,7 @@ public class ProjectResourceExceptionalTests : IAsyncLifetime new(_testProject.id, "My new name", ProjectVisibility.Public, "NonExistentWorkspace") ) ); - ex.InnerExceptions.Single().Should().BeOfType(); + ex.InnerExceptions.Single().Should().BeOfType(); } [Theory] diff --git a/tests/Speckle.Sdk.Tests.Unit/Api/GraphQLErrorHandler.cs b/tests/Speckle.Sdk.Tests.Unit/Api/GraphQLErrorHandler.cs index 2aa0b3bc..2e83e5c2 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Api/GraphQLErrorHandler.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Api/GraphQLErrorHandler.cs @@ -11,6 +11,7 @@ public class GraphQLErrorHandlerTests { yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } }]; yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } }]; + yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHORIZED_ACCESS_ERROR" } }]; yield return [typeof(SpeckleGraphQLInternalErrorException), new Map { { "code", "INTERNAL_SERVER_ERROR" } }]; yield return [typeof(SpeckleGraphQLStreamNotFoundException), new Map { { "code", "STREAM_NOT_FOUND" } }]; yield return [typeof(SpeckleGraphQLBadInputException), new Map { { "code", "BAD_USER_INPUT" } }]; From 0e98e1cccdb1b5987f8171f58d75d6453da825a4 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:00:51 +0000 Subject: [PATCH 13/17] refactor(ci): Refactor CI to run integration tests as separate workflow (#413) * Refactor CI to run integration tests as separate workflow * Tool restore * correct cache path * conditionally use container registry * use sln because net8 * fix typo * Correct trait filter * Correct mistake again * fix again * fml * clarify names * hopefully we're properly filtering test categories now * maybe this? * What does this do? * revert is test project changes * IsTestProject fix * Correct test setup for automate * maybe fix unit tests * docker-compose-file alighment * remove debug * Ok tests should now pass --- .github/workflows/integration-test.yml | 53 ++++++++ .github/workflows/pr.yml | 80 ++++++------ .github/workflows/release.yml | 11 +- Directory.Build.targets | 2 +- README.md | 6 +- Speckle.Sdk.sln | 1 + docker-compose-internal.yml | 118 ++++++++++++++++++ docker-compose.yml | 18 ++- src/Speckle.Sdk/Api/Exceptions.cs | 6 +- .../Api/GraphQL/GraphQLErrorHandler.cs | 5 +- .../Logging/SpecklePathProvider.cs | 4 +- .../Speckle.Sdk.Serialization.Testing.csproj | 1 + tests/Speckle.Sdk.Testing/MoqTest.cs | 14 ++- .../Speckle.Sdk.Testing.csproj | 2 +- .../Speckle.Sdk.Tests.Integration/Fixtures.cs | 2 + .../Speckle.Sdk.Tests.Performance.csproj | 2 +- .../Credentials/AccountManagerTests.cs | 4 +- 17 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/integration-test.yml create mode 100644 docker-compose-internal.yml diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 00000000..bde44de9 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,53 @@ +name: Integration Test + +on: + workflow_call: + inputs: + docker-compose-file: + required: true + type: string + use-github-container-registry: + default: false + type: boolean + +jobs: + integration-test: + env: + Solution: "Speckle.Sdk.sln" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.x.x + cache: true + cache-dependency-path: "**/packages.lock.json" + + - name: 🔐 Login to Github Container Registry + if: ${{ inputs.use-github-container-registry }} + uses: docker/login-action@v3 + with: + registry: "ghcr.io" + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: ⚙️ Spin up Server + run: docker compose -f ${{ inputs.docker-compose-file }} up --wait + + - name: 📦 Restore + run: dotnet restore ${{ env.Solution }} --locked-mode + + - name: 🏗️ Build + run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror + + - name: 🔨 Integration Tests + run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage + + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v5 + with: + files: tests/**/coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 59a80f0a..7712ecf6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,46 +1,52 @@ -name: .NET CI Build +name: PR Test on: pull_request: jobs: build: + env: + Solution: "Speckle.Sdk.sln" runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v5 + - name: Checkout + uses: actions/checkout@v5 - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 8.x.x - - - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - - - id: set-version - name: Set version to output - run: | - SEMVER="3.0.99.${{ github.run_number }}" - FILE_VERSION=$(echo "$SEMVER" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/') - FILE_VERSION="$FILE_VERSION.${{ github.run_number }}" - - echo "semver=$SEMVER" >> "$GITHUB_OUTPUT" - echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT" - - echo $SEMVER - echo $FILE_VERSION - - - name: 🔫 Build All - run: ./build.sh - env: - SEMVER: ${{ steps.set-version.outputs.SEMVER }} - FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }} - - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v5 - with: - files: tests/**/coverage.xml - token: ${{ secrets.CODECOV_TOKEN }} + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.x.x + cache: true + cache-dependency-path: "**/packages.lock.json" + + - name: 📦 Tool Restore + run: dotnet tool restore + + - name: 📄 Format + run: dotnet csharpier check . + + - name: 📦 Restore + run: dotnet restore ${{ env.Solution }} --locked-mode + + - name: 🏗️ Build + run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror + + - name: 🔨 Unit Tests + run: dotnet test ${{ env.Solution }} --configuration Release --filter "Category!=Integration" --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage + + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v5 + with: + files: tests/**/coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + + integration-test-internal: + uses: "./.github/workflows/integration-test.yml" + with: + docker-compose-file: "docker-compose-internal.yml" + use-github-container-registry: true + + integration-test-public: + uses: "./.github/workflows/integration-test.yml" + with: + docker-compose-file: "docker-compose.yml" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d41bbff7..5829da5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,9 @@ jobs: build: runs-on: ubuntu-latest environment: - name: 'nuget.org' + name: "nuget.org" permissions: - id-token: write # enable GitHub OIDC token issuance for this job + id-token: write # enable GitHub OIDC token issuance for this job steps: - name: Checkout @@ -20,11 +20,8 @@ jobs: uses: actions/setup-dotnet@v5 with: dotnet-version: 8.x.x - - - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + cache: true + cache-dependency-path: "**/packages.lock.json" - id: set-version name: Set version to output diff --git a/Directory.Build.targets b/Directory.Build.targets index 861d0313..1cb1832c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065; diff --git a/README.md b/README.md index 7f9bfd4a..b13f971f 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,12 @@ Docs are a bit patchy [https://docs.speckle.systems/developers/looking-for-devel ### Tests There are several test projects. It is a requirement that all tests pass for PRs to be merged. + The Integration test projects require a local server to be running. +You must have docker installed. Then you can run `docker compose up` from the root of the repo to start the required containers. -You must have docker installed. Then you can run `docker compose up --wait` from the root of the repo to start the required containers. - +In CI, they will be run against both the public and private versions of the server. +It is important that we remain compatible with both server versions. ## Contributing Before embarking on submitting a patch, please make sure you read: diff --git a/Speckle.Sdk.sln b/Speckle.Sdk.sln index b5b2b15d..9d91eac0 100644 --- a/Speckle.Sdk.sln +++ b/Speckle.Sdk.sln @@ -27,6 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{DA2AED CodeMetricsConfig.txt = CodeMetricsConfig.txt Directory.Build.Targets = Directory.Build.Targets .config\dotnet-tools.json = .config\dotnet-tools.json + docker-compose-internal.yml = docker-compose-internal.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{58D37DA9-F948-48CA-9A73-F5BBBD533DBF}" diff --git a/docker-compose-internal.yml b/docker-compose-internal.yml new file mode 100644 index 00000000..29974bae --- /dev/null +++ b/docker-compose-internal.yml @@ -0,0 +1,118 @@ +name: "speckle-server" + +services: + #### + # Speckle Server dependencies + ####### + postgres: + image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf" + restart: always + environment: + POSTGRES_DB: speckle + POSTGRES_USER: speckle + POSTGRES_PASSWORD: speckle + volumes: + - ./.volumes/postgres-data:/var/lib/postgresql/data/ + healthcheck: + # the -U user has to match the POSTGRES_USER value + test: ["CMD-SHELL", "pg_isready -U speckle"] + interval: 5s + timeout: 5s + retries: 30 + + redis: + image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4" + restart: always + volumes: + - ./.volumes/redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 5s + timeout: 5s + retries: 30 + + minio: + image: "minio/minio:RELEASE.2023-10-25T06-33-25Z" + command: server /data --console-address ":9001" + restart: always + volumes: + - ./.volumes/minio-data:/data + ports: + - '127.0.0.1:9000:9000' + - '127.0.0.1:9001:9001' + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html", + ] + interval: 5s + timeout: 30s + retries: 30 + start_period: 10s + + speckle-server: + image: ghcr.io/specklesystems/speckle-server:latest + restart: always + healthcheck: + test: + - CMD + - /nodejs/bin/node + - -e + - "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }" + interval: 10s + timeout: 10s + retries: 3 + start_period: 90s + ports: + - "0.0.0.0:3000:3000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + environment: + # TODO: Change this to the URL of the speckle server, as accessed from the network + CANONICAL_URL: "http://127.0.0.1:8080" + SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030" + FRONTEND_ORIGIN: "http://127.0.0.1:8081" + + # TODO: Change thvolumes: + REDIS_URL: "redis://redis" + + S3_ENDPOINT: "http://minio:9000" + S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000" + S3_ACCESS_KEY: "minioadmin" + S3_SECRET_KEY: "minioadmin" + S3_BUCKET: "speckle-server" + S3_CREATE_BUCKET: "true" + + FILE_SIZE_LIMIT_MB: 100 + MAX_PROJECT_MODELS_PER_PAGE: 500 + + # TODO: Change this to a unique secret for this server + SESSION_SECRET: "TODO:ReplaceWithLongString" + + STRATEGY_LOCAL: "true" + + POSTGRES_URL: "postgres" + POSTGRES_USER: "speckle" + POSTGRES_PASSWORD: "speckle" + POSTGRES_DB: "speckle" + ENABLE_MP: "false" + + LOG_PRETTY: "true" + + FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true" + FF_LARGE_FILE_IMPORTS_ENABLED: "true" + +networks: + default: + name: speckle-server + +volumes: + postgres-data: + redis-data: + minio-data: diff --git a/docker-compose.yml b/docker-compose.yml index b05dc03a..31e6006d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: POSTGRES_USER: speckle POSTGRES_PASSWORD: speckle volumes: - - postgres-data:/var/lib/postgresql/data/ + - ./.volumes/postgres-data:/var/lib/postgresql/data/ healthcheck: # the -U user has to match the POSTGRES_USER value test: ["CMD-SHELL", "pg_isready -U speckle"] @@ -24,7 +24,7 @@ services: image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4" restart: always volumes: - - redis-data:/data + - ./.volumes/redis-data:/data healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] interval: 5s @@ -36,7 +36,7 @@ services: command: server /data --console-address ":9001" restart: always volumes: - - minio-data:/data + - ./.volumes/minio-data:/data ports: - '127.0.0.1:9000:9000' - '127.0.0.1:9001:9001' @@ -55,7 +55,7 @@ services: image: speckle/speckle-server:latest restart: always healthcheck: - test: + test: - CMD - /nodejs/bin/node - -e @@ -81,9 +81,9 @@ services: # TODO: Change thvolumes: REDIS_URL: "redis://redis" - + S3_ENDPOINT: "http://minio:9000" - S3_PUBLIC_ENDPOINT: 'http://127.0.0.1:9000' + S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000" S3_ACCESS_KEY: "minioadmin" S3_SECRET_KEY: "minioadmin" S3_BUCKET: "speckle-server" @@ -96,19 +96,17 @@ services: SESSION_SECRET: "TODO:ReplaceWithLongString" STRATEGY_LOCAL: "true" - DEBUG: "speckle:*" POSTGRES_URL: "postgres" POSTGRES_USER: "speckle" POSTGRES_PASSWORD: "speckle" POSTGRES_DB: "speckle" ENABLE_MP: "false" - + LOG_PRETTY: "true" - + FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true" FF_LARGE_FILE_IMPORTS_ENABLED: "true" - networks: default: diff --git a/src/Speckle.Sdk/Api/Exceptions.cs b/src/Speckle.Sdk/Api/Exceptions.cs index 0e0de06c..ebae33db 100644 --- a/src/Speckle.Sdk/Api/Exceptions.cs +++ b/src/Speckle.Sdk/Api/Exceptions.cs @@ -23,11 +23,15 @@ public class SpeckleGraphQLException : SpeckleException } /// -/// Represents a "FORBIDDEN" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception. +/// Represents a "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception. /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden /// https://github.com/specklesystems/speckle-server/blob/v2.23.18/packages/server/modules/shared/errors/index.ts#L34 /// +/// +/// Server is a bit inconsistent with these error codes, hence there's 4 different codes that mean "auth no work" +/// Apollo no longer considers "FORBIDDEN" or "UNAUTHENTICATED" as built in error codes, so everything is custom anyway. +/// public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException { public SpeckleGraphQLForbiddenException() { } diff --git a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs index a2e4b54f..facda30a 100644 --- a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs +++ b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs @@ -28,9 +28,8 @@ internal static class GraphQLErrorHandler var ex = code switch { "GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message), - "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED_ACCESS_ERROR" => new SpeckleGraphQLForbiddenException( - message - ), + "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" => + new SpeckleGraphQLForbiddenException(message), "STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message), "BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message), "INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message), diff --git a/src/Speckle.Sdk/Logging/SpecklePathProvider.cs b/src/Speckle.Sdk/Logging/SpecklePathProvider.cs index 01b469ce..d488b40b 100644 --- a/src/Speckle.Sdk/Logging/SpecklePathProvider.cs +++ b/src/Speckle.Sdk/Logging/SpecklePathProvider.cs @@ -15,8 +15,8 @@ public static class SpecklePathProvider private const string ACCOUNTS_FOLDER_NAME = "Accounts"; - private static string UserDataPathEnvVar => "SPECKLE_USERDATA_PATH"; - private static string? Path => Environment.GetEnvironmentVariable(UserDataPathEnvVar); + public const string USER_DATA_PATH_ENV_VAR = "SPECKLE_USERDATA_PATH"; + private static string? Path => Environment.GetEnvironmentVariable(USER_DATA_PATH_ENV_VAR); /// /// Get the installation path. diff --git a/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj b/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj index b497eb60..99609e37 100644 --- a/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj +++ b/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/tests/Speckle.Sdk.Testing/MoqTest.cs b/tests/Speckle.Sdk.Testing/MoqTest.cs index 34e8391b..4aab3a39 100644 --- a/tests/Speckle.Sdk.Testing/MoqTest.cs +++ b/tests/Speckle.Sdk.Testing/MoqTest.cs @@ -8,7 +8,19 @@ public abstract class MoqTest : IDisposable { protected MoqTest() => Repository = new(MockBehavior.Strict); - public void Dispose() => Repository.VerifyAll(); + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + Repository.VerifyAll(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict); diff --git a/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj b/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj index 756d2c60..2390f7ec 100644 --- a/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj +++ b/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj @@ -1,7 +1,7 @@  net8.0 - true + true diff --git a/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs b/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs index 49af3a6a..3fcfd670 100644 --- a/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs +++ b/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs @@ -15,6 +15,8 @@ using Speckle.Sdk.Tests.Integration.API.GraphQL.Resources; using Speckle.Sdk.Transports; using Version = Speckle.Sdk.Api.GraphQL.Models.Version; +[assembly: AssemblyTrait("Category", "Integration")] + namespace Speckle.Sdk.Tests.Integration; public static class Fixtures diff --git a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj index c1217da2..0931e2e6 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj +++ b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj @@ -4,7 +4,7 @@ net8.0 enable disable - true + true diff --git a/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs b/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs index 9b23cae1..1e63ae19 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs @@ -9,7 +9,7 @@ using Speckle.Sdk.Testing; namespace Speckle.Sdk.Tests.Unit.Credentials; -public class AccountManagerTests : MoqTest +public sealed class AccountManagerTests : MoqTest { private class TestAccountFactory : IAccountFactory { @@ -36,7 +36,9 @@ public class AccountManagerTests : MoqTest private readonly Mock _mockAccountStorage; private readonly Mock _mockAccountAddLockStorage; +#pragma warning disable CA2213 private readonly AccountManager _accountManager; +#pragma warning restore CA2213 public AccountManagerTests() { From 67236abafe13a08c770d2cabd951b5efe2122a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Wed, 29 Oct 2025 14:39:06 +0200 Subject: [PATCH 14/17] refactor: rootCollection to use IProperties --- src/Speckle.Objects/Interfaces.cs | 11 ++--------- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 9 ++++----- src/Speckle.Sdk/Models/Proxies/IProperties.cs | 10 ++++++++++ 3 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 src/Speckle.Sdk/Models/Proxies/IProperties.cs diff --git a/src/Speckle.Objects/Interfaces.cs b/src/Speckle.Objects/Interfaces.cs index b2df1903..5d41a500 100644 --- a/src/Speckle.Objects/Interfaces.cs +++ b/src/Speckle.Objects/Interfaces.cs @@ -2,6 +2,7 @@ using Speckle.Objects.Geometry; using Speckle.Objects.Other; using Speckle.Objects.Primitive; using Speckle.Sdk.Models; +using Speckle.Sdk.Models.Proxies; namespace Speckle.Objects; @@ -110,15 +111,7 @@ public interface IDisplayValue : ISpeckleObject #region Data objects -/// -/// Specifies properties on objects to be used for data-based workflows -/// -public interface IProperties : ISpeckleObject -{ - Dictionary properties { get; } -} - -public interface IDataObject : IProperties, IDisplayValue> +public interface IDataObject : IProperties, IDisplayValue>, ISpeckleObject { /// /// The name of the object, primarily used to decorate the object for consumption in frontend and other apps diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index ace5979c..7082ba6b 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -1,3 +1,5 @@ +using Speckle.Sdk.Models.Proxies; + namespace Speckle.Sdk.Models.Collections; /// @@ -5,7 +7,7 @@ namespace Speckle.Sdk.Models.Collections; /// Extends Collection to include model-wide properties that apply to the entire model. /// [SpeckleType("Speckle.Core.Models.Collections.RootCollection")] -public class RootCollection : Collection +public class RootCollection : Collection, IProperties { public RootCollection() { } @@ -15,8 +17,5 @@ public class RootCollection : Collection /// /// Model-wide properties that apply to the entire model. /// - /// - /// These are intended for model-level metadata such as total area, project information, or analysis results. - /// - public Dictionary? properties { get; set; } + public Dictionary properties { get; set; } = new(); } diff --git a/src/Speckle.Sdk/Models/Proxies/IProperties.cs b/src/Speckle.Sdk/Models/Proxies/IProperties.cs new file mode 100644 index 00000000..1a1fc252 --- /dev/null +++ b/src/Speckle.Sdk/Models/Proxies/IProperties.cs @@ -0,0 +1,10 @@ +namespace Speckle.Sdk.Models.Proxies; + +/// +/// Specifies properties on objects to be used for data-based workflows. +/// Can be applied to both objects and collections. +/// +public interface IProperties +{ + Dictionary properties { get; } +} From cb31fd1a089a53802a5ef00c501d0f6ef044c36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinhagen?= Date: Wed, 5 Nov 2025 12:18:40 +0200 Subject: [PATCH 15/17] chore: namespace change --- src/Speckle.Objects/Interfaces.cs | 1 + src/Speckle.Sdk/Models/Collections/RootCollection.cs | 1 + src/Speckle.Sdk/Models/{Proxies => Data}/IProperties.cs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename src/Speckle.Sdk/Models/{Proxies => Data}/IProperties.cs (86%) diff --git a/src/Speckle.Objects/Interfaces.cs b/src/Speckle.Objects/Interfaces.cs index 5d41a500..6a35fa30 100644 --- a/src/Speckle.Objects/Interfaces.cs +++ b/src/Speckle.Objects/Interfaces.cs @@ -2,6 +2,7 @@ using Speckle.Objects.Geometry; using Speckle.Objects.Other; using Speckle.Objects.Primitive; using Speckle.Sdk.Models; +using Speckle.Sdk.Models.Data; using Speckle.Sdk.Models.Proxies; namespace Speckle.Objects; diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index 7082ba6b..da7d204c 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -1,3 +1,4 @@ +using Speckle.Sdk.Models.Data; using Speckle.Sdk.Models.Proxies; namespace Speckle.Sdk.Models.Collections; diff --git a/src/Speckle.Sdk/Models/Proxies/IProperties.cs b/src/Speckle.Sdk/Models/Data/IProperties.cs similarity index 86% rename from src/Speckle.Sdk/Models/Proxies/IProperties.cs rename to src/Speckle.Sdk/Models/Data/IProperties.cs index 1a1fc252..cf0e381c 100644 --- a/src/Speckle.Sdk/Models/Proxies/IProperties.cs +++ b/src/Speckle.Sdk/Models/Data/IProperties.cs @@ -1,4 +1,4 @@ -namespace Speckle.Sdk.Models.Proxies; +namespace Speckle.Sdk.Models.Data; /// /// Specifies properties on objects to be used for data-based workflows. From 42c26e38bf3b3142f2ce60fc0aca20c29bf2af22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinhagen?= Date: Wed, 5 Nov 2025 12:22:27 +0200 Subject: [PATCH 16/17] fix: unnecessary using directive --- src/Speckle.Sdk/Models/Collections/RootCollection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Speckle.Sdk/Models/Collections/RootCollection.cs b/src/Speckle.Sdk/Models/Collections/RootCollection.cs index da7d204c..ca3c9904 100644 --- a/src/Speckle.Sdk/Models/Collections/RootCollection.cs +++ b/src/Speckle.Sdk/Models/Collections/RootCollection.cs @@ -1,5 +1,4 @@ using Speckle.Sdk.Models.Data; -using Speckle.Sdk.Models.Proxies; namespace Speckle.Sdk.Models.Collections; From 2ea63486899fb2822d8f1037e21218d6ce61eb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinhagen?= Date: Wed, 5 Nov 2025 12:24:06 +0200 Subject: [PATCH 17/17] fix: again :/ --- src/Speckle.Objects/Interfaces.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Speckle.Objects/Interfaces.cs b/src/Speckle.Objects/Interfaces.cs index 6a35fa30..9effeb50 100644 --- a/src/Speckle.Objects/Interfaces.cs +++ b/src/Speckle.Objects/Interfaces.cs @@ -3,7 +3,6 @@ using Speckle.Objects.Other; using Speckle.Objects.Primitive; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Data; -using Speckle.Sdk.Models.Proxies; namespace Speckle.Objects;