Compare commits

...

10 Commits

Author SHA1 Message Date
kekesidavid 0361a6e5b7 Merge pull request #318 from specklesystems/david/move-text-class-update-pr-to-main
.NET Build and Publish / build (push) Has been cancelled
fix (sdk) Text class updates: removed origin from Text class and added screenAligned prop
2025-06-02 09:22:57 +02:00
David Kekesi 0e97782c29 fixed comment 2025-06-02 09:12:12 +02:00
David Kekesi 298dedc3af text class update pr moved to main 2025-05-30 19:01:02 +02:00
Jedd Morgan efc38d8f5c Added Workspace project visibility (#307)
.NET Build and Publish / build (push) Has been cancelled
2025-05-14 21:37:39 +03:00
Adam Hathcock 8d3985f93b Merge pull request #305 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Dev to Main
2025-05-14 10:19:51 +01:00
Jedd Morgan 915a18dc98 Merge pull request #303 from specklesystems/jrm/main-dev
Main -> Dev
2025-05-13 11:38:56 +01:00
Jedd Morgan 5fcb3223d6 Merge branch 'dev' into jrm/main-dev 2025-05-13 11:28:13 +01:00
Adam Hathcock 21851c06d2 Use WhenAll instead of WhenAny and avoid manual task exception processing (#300)
* No reason to process exceptions manually

* formatting

* use a pool for gathering child task results

* Use a smaller whenany with a cancellation

* formatting
2025-05-13 10:00:53 +01:00
Adam Hathcock 3a9a633d30 Add empty DataObject tests (#301)
* Add empty DataObject tests

* format
2025-05-09 14:51:03 +00:00
Jedd Morgan 7f092d529c feat(api): Add ActiveUserResource.GetProjectsWithPermissions (#299)
.NET Build and Publish / build (push) Has been cancelled
* Fixed Mistakes (#296)

* Added extra permission checks (#297)

* Add extra query for project with permissions

---------

Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
Co-authored-by: Adam Hathcock <adam@hathcock.uk>
2025-05-08 15:11:53 +00:00
20 changed files with 280 additions and 37 deletions
+2
View File
@@ -100,6 +100,8 @@ services:
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
networks:
default:
+7 -8
View File
@@ -1,6 +1,5 @@
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
using Point = Speckle.Objects.Geometry.Point;
namespace Speckle.Objects.Annotation;
@@ -15,11 +14,6 @@ public class Text : Base
/// </summary>
public required string value { get; set; }
/// <summary>
/// Origin point, relation to the text is defined by AlignmentHorizontal and AlignmentVertical
/// </summary>
public required Point origin { get; set; }
/// <summary>
/// Height in linear units or pixels (if Units.None)
/// </summary>
@@ -31,6 +25,11 @@ public class Text : Base
/// </summary>
public required string units { get; set; }
/// <summary>
/// If true, the text is oriented to face the screen (camera-aligned).
/// </summary>
public required bool screenOriented { get; set; }
/// <summary>
/// Horizontal alignment: Left, Center or Right
/// </summary>
@@ -42,9 +41,9 @@ public class Text : Base
public AlignmentVertical alignmentV { get; set; }
/// <summary>
/// Plane will be null if the text object orientation follows camera view
/// Plane axis vectors will be ignored if screenOriented is true
/// </summary>
public Plane? plane { get; set; }
public required Plane plane { get; set; }
/// <summary>
/// Maximum width of the text field (in 'units').
@@ -1,10 +1,11 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
namespace Speckle.Sdk.Api.GraphQL.Enums;
public enum ProjectVisibility
{
Private = 0,
Private,
Public,
[Obsolete("Use Unlisted instead", true)]
Public = 1,
Unlisted = 2,
[Obsolete("Use Public instead")]
Unlisted,
Workspace,
}
@@ -26,3 +26,8 @@ public sealed class ProjectWithTeam : Project
public List<PendingStreamCollaborator> invitedTeam { get; init; }
public List<ProjectCollaborator> team { get; init; }
}
public sealed class ProjectWithPermissions : Project
{
public ProjectPermissionChecks permissions { get; init; }
}
@@ -361,4 +361,89 @@ public sealed class ActiveUserResource
return response.data.data;
}
/// <param name="limit">Max number of projects to fetch</param>
/// <param name="cursor">Optional cursor for pagination</param>
/// <param name="filter">Optional filter</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
public async Task<ResourceCollection<ProjectWithPermissions>> GetProjectsWithPermissions(
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
string? cursor = null,
UserProjectsFilter? filter = null,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
query User($limit: Int!, $cursor: String, $filter: UserProjectsFilter) {
data: activeUser {
data: projects(limit: $limit, cursor: $cursor, filter: $filter) {
totalCount
cursor
items {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
permissions {
canCreateModel {
code
authorized
message
}
canDelete {
code
authorized
message
}
canLoad {
code
authorized
message
}
canPublish {
code
authorized
message
}
}
}
}
}
}
""";
var request = new GraphQLRequest
{
Query = QUERY,
Variables = new
{
limit,
cursor,
filter,
},
};
var response = await _client
.ExecuteGraphQLRequest<NullableResponse<RequiredResponse<ResourceCollection<ProjectWithPermissions>>?>>(
request,
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;
}
}
@@ -69,6 +69,10 @@ public sealed class SerializeProcess(
NodeInfo
>();
private readonly Pool<List<Task<Dictionary<Id, NodeInfo>>>> _taskResultPool = Pools.CreateListPool<
Task<Dictionary<Id, NodeInfo>>
>();
private long _objectCount;
private long _objectsFound;
@@ -163,7 +167,7 @@ public sealed class SerializeProcess(
try
{
var tasks = new List<Task<Dictionary<Id, NodeInfo>>>();
var tasks = _taskResultPool.Get();
foreach (var child in baseChildFinder.GetChildren(obj))
{
// tmp is necessary because of the way closures close over loop variables
@@ -190,30 +194,27 @@ public sealed class SerializeProcess(
return EMPTY_CLOSURES;
}
List<Dictionary<Id, NodeInfo>> taskClosures = new();
Dictionary<Id, NodeInfo>[] taskClosures = [];
if (tasks.Count > 0)
{
var currentTasks = tasks.ToList();
do
//get child results
var childTask = Task.WhenAll(tasks);
await Task.WhenAny(childTask, Task.Delay(Timeout.InfiniteTimeSpan, _processSource.Token)).ConfigureAwait(false);
if (childTask.IsFaulted)
{
//grab when any Task is done and see if we're cancelling
var t = await Task.WhenAny(currentTasks).ConfigureAwait(false);
if (t.IsCanceled)
if (childTask.Exception is not null)
{
return EMPTY_CLOSURES;
RecordException(childTask.Exception);
}
if (t.IsFaulted)
{
if (t.Exception is not null)
{
RecordException(t.Exception);
}
return EMPTY_CLOSURES;
}
taskClosures.Add(t.Result);
currentTasks.Remove(t);
} while (currentTasks.Count > 0);
return EMPTY_CLOSURES;
}
if (!childTask.IsCompleted)
{
return EMPTY_CLOSURES;
}
taskClosures = childTask.Result;
}
_taskResultPool.Return(tasks);
if (_processSource.Token.IsCancellationRequested)
{
@@ -0,0 +1,10 @@
{
"applicationId": null,
"displayValue": null,
"id": "15168a13ce3f336dee9aa1807cbf375c",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.ArcgisObject",
"type": null,
"units": null
}
@@ -0,0 +1,11 @@
{
"applicationId": null,
"displayValue": null,
"elements": null,
"id": "bf80e8a10eca2264f11c39bae42538da",
"level": null,
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.ArchicadObject",
"type": null
}
@@ -0,0 +1,12 @@
{
"applicationId": null,
"baseCurves": null,
"displayValue": null,
"elements": null,
"id": "76b7634117981a9fb9d3cffca5464f26",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.Civil3dObject",
"type": null,
"units": null
}
@@ -0,0 +1,8 @@
{
"applicationId": null,
"displayValue": null,
"id": "a1567a1cf10417294c93b70bf5ca97c1",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject"
}
@@ -0,0 +1,11 @@
{
"applicationId": null,
"displayValue": null,
"elements": null,
"id": "39d4deaa7cd20e7004812304f41a68d5",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.EtabsObject",
"type": null,
"units": null
}
@@ -0,0 +1,9 @@
{
"applicationId": null,
"displayValue": null,
"id": "fbda4ea7bb1b3722ca28e97573742a4e",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.NavisworksObject",
"units": null
}
@@ -0,0 +1,15 @@
{
"applicationId": null,
"category": null,
"displayValue": null,
"elements": null,
"family": null,
"id": "7e285508a71c55589bbc053451f687d2",
"level": null,
"location": null,
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.RevitObject",
"type": null,
"units": null
}
@@ -0,0 +1,11 @@
{
"applicationId": null,
"displayValue": null,
"elements": null,
"id": "c07f15678d8b4a48a7ecab9eb30e69a8",
"name": null,
"properties": null,
"speckle_type": "Objects.Data.DataObject:Objects.Data.TeklaObject",
"type": null,
"units": null
}
@@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Objects.Data;
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
using Speckle.Sdk.Serialisation;
using Speckle.Sdk.Serialisation.V2;
using Speckle.Sdk.Serialisation.V2.Send;
namespace Speckle.Sdk.Serialization.Tests;
public class DataObjectTests
{
private readonly ISerializeProcessFactory _factory;
public DataObjectTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSpeckleSdk(new("Tests", "test"), "v3", typeof(TestClass).Assembly, typeof(Polyline).Assembly);
var serviceProvider = serviceCollection.BuildServiceProvider();
_factory = serviceProvider.GetRequiredService<ISerializeProcessFactory>();
}
[Theory]
[InlineData(typeof(ArcgisObject))]
[InlineData(typeof(ArchicadObject))]
[InlineData(typeof(Civil3dObject))]
[InlineData(typeof(DataObject))]
[InlineData(typeof(EtabsObject))]
[InlineData(typeof(NavisworksObject))]
[InlineData(typeof(RevitObject))]
[InlineData(typeof(TeklaObject))]
public async Task ValidateDataObject(Type type)
{
Base x = (Base)(Activator.CreateInstance(type) ?? throw new Exception("Could not create instance of " + type.Name));
var json = new ConcurrentDictionary<Id, Json>();
await using var serializeProcess = _factory.CreateSerializeProcess(
new MemoryJsonCacheManager(json),
new DummyServerObjectManager(),
null,
default,
new SerializeProcessOptions(true, true, false, true)
);
await serializeProcess.Serialize(x);
await VerifyJson(json.Single().Value.Value).UseParameters(type);
}
}
@@ -66,6 +66,19 @@ public class ActiveUserResourceTests : IAsyncLifetime
res.items.Count.Should().Be(2);
}
[Fact]
public async Task ActiveUserGetProjectsWithPermissions()
{
var p1 = await _testUser.Project.Create(new("Project 3", null, null));
var p2 = await _testUser.Project.Create(new("Project 4", null, null));
var res = await Sut.GetProjectsWithPermissions();
res.items.Should().Contain(x => x.id == p1.id);
res.items.Should().Contain(x => x.id == p2.id);
res.items.Count.Should().Be(2);
}
[Fact]
public async Task ActiveUserGetProjects_NoAuth()
{
@@ -1,6 +1,7 @@
using FluentAssertions;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Common;
@@ -18,7 +19,7 @@ public class ProjectInviteResourceTests : IAsyncLifetime
{
_inviter = await Fixtures.SeedUserWithClient();
_invitee = await Fixtures.SeedUserWithClient();
_project = await _inviter.Project.Create(new("test", null, null));
_project = await _inviter.Project.Create(new("test", null, ProjectVisibility.Public));
_createdInvite = await SeedInvite();
}
@@ -90,7 +90,7 @@ public class ProjectResourceExceptionalTests : IAsyncLifetime
{
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
_ = await _unauthedUser.Project.CreateInWorkspace(
new(_testProject.id, "My new name", ProjectVisibility.Unlisted, "NonExistentWorkspace")
new(_testProject.id, "My new name", ProjectVisibility.Public, "NonExistentWorkspace")
)
);
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLException>();
@@ -29,7 +29,7 @@ public class ProjectResourceTests
[Theory]
[InlineData("Very private project", "My secret project", ProjectVisibility.Private)]
[InlineData("Very unlisted project", null, ProjectVisibility.Unlisted)]
[InlineData("Very unlisted project", null, ProjectVisibility.Public)]
public async Task ProjectCreate_Should_CreateProjectSuccessfully(
string name,
string? description,
@@ -70,7 +70,7 @@ public class ProjectResourceTests
// Arrange
const string NEW_NAME = "MY new name";
const string NEW_DESCRIPTION = "MY new desc";
const ProjectVisibility NEW_VISIBILITY = ProjectVisibility.Unlisted;
const ProjectVisibility NEW_VISIBILITY = ProjectVisibility.Public;
// Act
var newProject = await Sut.Update(
@@ -57,7 +57,7 @@ public class GeneralSendTest
client = TestDataHelper.ServiceProvider.GetRequiredService<IClientFactory>().Create(acc);
_project = await client.Project.Create(
new($"General Send Test run {Guid.NewGuid()}", null, ProjectVisibility.Unlisted)
new($"General Send Test run {Guid.NewGuid()}", null, ProjectVisibility.Public)
);
_remote = TestDataHelper.ServiceProvider.GetRequiredService<IServerTransportFactory>().Create(acc, _project.id);
}