diff --git a/Automate/Speckle.Automate.Sdk/AutomationContext.cs b/Automate/Speckle.Automate.Sdk/AutomationContext.cs index 5844df540a..bacf101565 100644 --- a/Automate/Speckle.Automate.Sdk/AutomationContext.cs +++ b/Automate/Speckle.Automate.Sdk/AutomationContext.cs @@ -3,6 +3,7 @@ using Speckle.Automate.Sdk.Schema; using Speckle.Automate.Sdk.Schema.Triggers; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; using Speckle.Core.Logging; using Speckle.Core.Models; diff --git a/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj b/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj index bbdb0d9a65..3b9b37c95e 100644 --- a/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj +++ b/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj @@ -13,7 +13,7 @@ true - $(WarningsNotAsErrors);CS8618; + $(WarningsNotAsErrors);CS8618;CS0618; diff --git a/Core/Core/Api/Exceptions.cs b/Core/Core/Api/Exceptions.cs index 7d10bd9974..f100fd22c3 100644 --- a/Core/Core/Api/Exceptions.cs +++ b/Core/Core/Api/Exceptions.cs @@ -9,50 +9,55 @@ namespace Speckle.Core.Api; /// /// Base class for GraphQL API exceptions /// -public class SpeckleGraphQLException : SpeckleException +public class SpeckleGraphQLException : SpeckleGraphQLException { - private readonly GraphQLRequest _request; - public GraphQLResponse? Response { get; } - - public SpeckleGraphQLException(string message, GraphQLRequest request, GraphQLResponse? response) - : base(message) - { - _request = request; - Response = response; - } + public new GraphQLResponse? Response => (GraphQLResponse?)base.Response; - public SpeckleGraphQLException(string message, Exception inner, GraphQLRequest request, GraphQLResponse? response) - : this(message, inner) - { - _request = request; - Response = response; - } + public SpeckleGraphQLException( + string message, + GraphQLRequest request, + GraphQLResponse? response, + Exception? innerException = null + ) + : base(message, request, response, innerException) { } public SpeckleGraphQLException() { } - public SpeckleGraphQLException(string message) + public SpeckleGraphQLException(string? message) : base(message) { } - public SpeckleGraphQLException(string message, Exception innerException) + public SpeckleGraphQLException(string? message, Exception? innerException) : base(message, innerException) { } +} + +public class SpeckleGraphQLException : SpeckleException +{ + private readonly GraphQLRequest _request; + public IGraphQLResponse? Response { get; } public IEnumerable ErrorMessages => Response?.Errors != null ? Response.Errors.Select(e => e.Message) : Enumerable.Empty(); public IDictionary? Extensions => Response?.Extensions; -} -public class SpeckleGraphQLException : SpeckleGraphQLException -{ - public SpeckleGraphQLException(string message, GraphQLRequest request, GraphQLResponse? response) - : base(message, request, response) { } + public SpeckleGraphQLException( + string? message, + GraphQLRequest request, + IGraphQLResponse? response, + Exception? innerException = null + ) + : base(message, innerException) + { + _request = request; + Response = response; + } public SpeckleGraphQLException() { } - public SpeckleGraphQLException(string message) + public SpeckleGraphQLException(string? message) : base(message) { } - public SpeckleGraphQLException(string message, Exception innerException) + public SpeckleGraphQLException(string? message, Exception? innerException) : base(message, innerException) { } } @@ -61,44 +66,56 @@ public SpeckleGraphQLException(string message, Exception innerException) /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated /// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden /// -public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException +public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException { - public SpeckleGraphQLForbiddenException(GraphQLRequest request, GraphQLResponse response) - : base("Your request was forbidden", request, response) { } + public SpeckleGraphQLForbiddenException( + GraphQLRequest request, + IGraphQLResponse response, + Exception? innerException = null + ) + : base("Your request was forbidden", request, response, innerException) { } public SpeckleGraphQLForbiddenException() { } - public SpeckleGraphQLForbiddenException(string message) + public SpeckleGraphQLForbiddenException(string? message) : base(message) { } - public SpeckleGraphQLForbiddenException(string message, Exception innerException) + public SpeckleGraphQLForbiddenException(string? message, Exception? innerException) : base(message, innerException) { } } -public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException +public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException { - public SpeckleGraphQLInternalErrorException(GraphQLRequest request, GraphQLResponse response) - : base("Your request failed on the server side", request, response) { } + public SpeckleGraphQLInternalErrorException( + GraphQLRequest request, + IGraphQLResponse response, + Exception? innerException = null + ) + : base("Your request failed on the server side", request, response, innerException) { } public SpeckleGraphQLInternalErrorException() { } - public SpeckleGraphQLInternalErrorException(string message) + public SpeckleGraphQLInternalErrorException(string? message) : base(message) { } - public SpeckleGraphQLInternalErrorException(string message, Exception innerException) + public SpeckleGraphQLInternalErrorException(string? message, Exception? innerException) : base(message, innerException) { } } -public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException +public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException { - public SpeckleGraphQLStreamNotFoundException(GraphQLRequest request, GraphQLResponse response) - : base("Stream not found", request, response) { } + public SpeckleGraphQLStreamNotFoundException( + GraphQLRequest request, + IGraphQLResponse response, + Exception? innerException = null + ) + : base("Stream not found", request, response, innerException) { } public SpeckleGraphQLStreamNotFoundException() { } - public SpeckleGraphQLStreamNotFoundException(string message) + public SpeckleGraphQLStreamNotFoundException(string? message) : base(message) { } - public SpeckleGraphQLStreamNotFoundException(string message, Exception innerException) + public SpeckleGraphQLStreamNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } } diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObsoleteOperations.cs b/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObsoleteOperations.cs deleted file mode 100644 index 15d0054f0e..0000000000 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObsoleteOperations.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using GraphQL; - -namespace Speckle.Core.Api; - -[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last")] -public partial class Client -{ - #region Stream Grant Permission - - /// - /// Grants permissions to a user on a given stream. - /// - /// - /// - [Obsolete("Please use the `StreamUpdatePermission` method", true)] - public Task StreamGrantPermission(StreamPermissionInput permissionInput) - { - return StreamGrantPermission(CancellationToken.None, permissionInput); - } - - /// - /// Grants permissions to a user on a given stream. - /// - /// - /// - /// - [Obsolete("Please use the `StreamUpdatePermission` method", true)] - public async Task StreamGrantPermission( - CancellationToken cancellationToken, - StreamPermissionInput permissionInput - ) - { - var request = new GraphQLRequest - { - Query = - @" - mutation streamGrantPermission($permissionParams: StreamGrantPermissionInput!) { - streamGrantPermission(permissionParams:$permissionParams) - }", - Variables = new { permissionParams = permissionInput } - }; - - var res = await ExecuteGraphQLRequest>(request, cancellationToken).ConfigureAwait(false); - return (bool)res["streamGrantPermission"]; - } - - #endregion - - #region Cancellation token as last param - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> StreamGetActivity( - CancellationToken cancellationToken, - string id, - DateTime? after = null, - DateTime? before = null, - DateTime? cursor = null, - string actionType = "", - int limit = 25 - ) - { - return StreamGetActivity(id, after, before, cursor, actionType, limit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> StreamGetBranches( - CancellationToken cancellationToken, - string streamId, - int branchesLimit = 10, - int commitsLimit = 10 - ) - { - return StreamGetBranches(streamId, branchesLimit, commitsLimit, CancellationToken.None); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task BranchCreate(CancellationToken cancellationToken, BranchCreateInput branchInput) - { - return BranchCreate(branchInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task BranchGet( - CancellationToken cancellationToken, - string streamId, - string branchName, - int commitsLimit = 10 - ) - { - return BranchGet(streamId, branchName, commitsLimit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task BranchUpdate(CancellationToken cancellationToken, BranchUpdateInput branchInput) - { - return BranchUpdate(branchInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task BranchDelete(CancellationToken cancellationToken, BranchDeleteInput branchInput) - { - return BranchDelete(branchInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamGetComments( - CancellationToken cancellationToken, - string streamId, - int limit = 25, - string? cursor = null - ) - { - return StreamGetComments(streamId, limit, cursor, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamGetCommentScreenshot(CancellationToken cancellationToken, string id, string streamId) - { - return StreamGetCommentScreenshot(id, streamId, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task CommitGet(CancellationToken cancellationToken, string streamId, string commitId) - { - return CommitGet(streamId, commitId, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> StreamGetCommits(CancellationToken cancellationToken, string streamId, int limit = 10) - { - return StreamGetCommits(streamId, limit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task CommitCreate(CancellationToken cancellationToken, CommitCreateInput commitInput) - { - return CommitCreate(commitInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task CommitUpdate(CancellationToken cancellationToken, CommitUpdateInput commitInput) - { - return CommitUpdate(commitInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task CommitDelete(CancellationToken cancellationToken, CommitDeleteInput commitInput) - { - return CommitDelete(commitInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task CommitReceived(CancellationToken cancellationToken, CommitReceivedInput commitReceivedInput) - { - return CommitReceived(commitReceivedInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task ObjectGet(CancellationToken cancellationToken, string streamId, string objectId) - { - return ObjectGet(streamId, objectId, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task ObjectCountGet(CancellationToken cancellationToken, string streamId, string objectId) - { - return ObjectCountGet(streamId, objectId, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamGet(CancellationToken cancellationToken, string id, int branchesLimit = 10) - { - return StreamGet(id, branchesLimit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> StreamsGet(CancellationToken cancellationToken, int limit = 10) - { - return StreamsGet(limit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> FavoriteStreamsGet(CancellationToken cancellationToken, int limit = 10) - { - return FavoriteStreamsGet(limit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> StreamSearch(CancellationToken cancellationToken, string query, int limit = 10) - { - return StreamSearch(query, limit, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamCreate(CancellationToken cancellationToken, StreamCreateInput streamInput) - { - return StreamCreate(streamInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamUpdate(CancellationToken cancellationToken, StreamUpdateInput streamInput) - { - return StreamUpdate(streamInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamDelete(CancellationToken cancellationToken, string id) - { - return StreamDelete(id, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamRevokePermission( - CancellationToken cancellationToken, - StreamRevokePermissionInput permissionInput - ) - { - return StreamRevokePermission(permissionInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamGetPendingCollaborators(CancellationToken cancellationToken, string id) - { - return StreamGetPendingCollaborators(id, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task StreamInviteCreate(CancellationToken cancellationToken, StreamInviteCreateInput inviteCreateInput) - { - return StreamInviteCreate(inviteCreateInput, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task OtherUserGet(CancellationToken cancellationToken, string id) - { - return OtherUserGet(id, cancellationToken); - } - - [Obsolete("Use overload with cancellation token parameter last")] - public Task> UserSearch(CancellationToken cancellationToken, string query, int limit = 10) - { - return UserSearch(query, limit, cancellationToken); - } - #endregion -} diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.UserOperations.cs b/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.UserOperations.cs deleted file mode 100644 index a79764e551..0000000000 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.UserOperations.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using GraphQL; - -namespace Speckle.Core.Api; - -public partial class Client -{ - /// - /// Gets the currently active user profile. - /// - /// - /// - public async Task ActiveUserGet(CancellationToken cancellationToken = default) - { - var request = new GraphQLRequest - { - Query = - @"query User { - activeUser { - id, - email, - name, - bio, - company, - avatar, - verified, - profiles, - role, - } - }" - }; - return (await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false)).activeUser; - } - - /// - /// Get another user's profile by its user id. - /// - /// Id of the user you are looking for - /// - /// - public async Task OtherUserGet(string id, CancellationToken cancellationToken = default) - { - var request = new GraphQLRequest - { - Query = - @"query LimitedUser($id: String!) { - otherUser(id: $id){ - id, - name, - bio, - company, - avatar, - verified, - role, - } - }", - Variables = new { id } - }; - return (await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false)).otherUser; - } - - /// - /// Searches for a user on the server. - /// - /// String to search for. Must be at least 3 characters - /// Max number of users to return - /// - public async Task> UserSearch( - string query, - int limit = 10, - CancellationToken cancellationToken = default - ) - { - var request = new GraphQLRequest - { - Query = - @"query UserSearch($query: String!, $limit: Int!) { - userSearch(query: $query, limit: $limit) { - cursor, - items { - id - name - bio - company - avatar - verified - role - } - } - }", - Variables = new { query, limit } - }; - return (await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false)) - .userSearch - .items; - } -} diff --git a/Core/Core/Api/GraphQL/Client.cs b/Core/Core/Api/GraphQL/Client.cs index bacfa63bc0..9a645006f4 100644 --- a/Core/Core/Api/GraphQL/Client.cs +++ b/Core/Core/Api/GraphQL/Client.cs @@ -15,6 +15,9 @@ using Serilog.Context; using Serilog.Core; using Serilog.Core.Enrichers; +using Serilog.Events; +using Speckle.Core.Api.GraphQL; +using Speckle.Core.Api.GraphQL.Resources; using Speckle.Core.Api.GraphQL.Serializer; using Speckle.Core.Credentials; using Speckle.Core.Helpers; @@ -23,71 +26,55 @@ namespace Speckle.Core.Api; -public sealed partial class Client : IDisposable +public sealed partial class Client : ISpeckleGraphQLClient, IDisposable { - [Obsolete] - internal Client() { } + public ProjectResource Project { get; } + public ModelResource Model { get; } + public VersionResource Version { get; } + public ActiveUserResource ActiveUser { get; } + public OtherUserResource OtherUser { get; } + public ProjectInviteResource ProjectInvite { get; } + public CommentResource Comment { get; } + public SubscriptionResource Subscription { get; } - public Client(Account account) - { - Account = account ?? throw new SpeckleException("Provided account is null."); - - HttpClient = Http.GetHttpProxyClient(null, TimeSpan.FromSeconds(30)); - Http.AddAuthHeader(HttpClient, account.token); + public string ServerUrl => Account.serverInfo.url; - HttpClient.DefaultRequestHeaders.Add("apollographql-client-name", Setup.HostApplication); - HttpClient.DefaultRequestHeaders.Add( - "apollographql-client-version", - Assembly.GetExecutingAssembly().GetName().Version.ToString() - ); + public string ApiToken => Account.token; - GQLClient = new GraphQLHttpClient( - new GraphQLHttpClientOptions - { - EndPoint = new Uri(new Uri(account.serverInfo.url), "/graphql"), - UseWebSocketForQueriesAndMutations = false, - WebSocketProtocol = "graphql-ws", - ConfigureWebSocketConnectionInitPayload = _ => - { - return Http.CanAddAuth(account.token, out string? authValue) ? new { Authorization = authValue } : null; - }, - }, - new NewtonsoftJsonSerializer(), - HttpClient - ); + public System.Version? ServerVersion { get; private set; } - GQLClient.WebSocketReceiveErrors.Subscribe(e => - { - if (e is WebSocketException we) - { - Console.WriteLine( - $"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}" - ); - } - else - { - Console.WriteLine($"Exception in websocket receive stream: {e}"); - } - }); - } + [JsonIgnore] + public Account Account { get; } - public string ServerUrl => Account.serverInfo.url; + private HttpClient HttpClient { get; } - public string ApiToken => Account.token; + public GraphQLHttpClient GQLClient { get; } - public System.Version? ServerVersion { get; set; } + /// + /// was null + public Client(Account account) + { + Account = account ?? throw new ArgumentException("Provided account is null."); - [JsonIgnore] - public Account Account { get; set; } + Project = new(this); + Model = new(this); + Version = new(this); + ActiveUser = new(this); + OtherUser = new(this); + ProjectInvite = new(this); + Comment = new(this); + Subscription = new(this); - private HttpClient HttpClient { get; set; } + HttpClient = CreateHttpClient(account); - public GraphQLHttpClient GQLClient { get; set; } + GQLClient = CreateGraphQLClient(account, HttpClient); + } public void Dispose() { try { + Subscription.Dispose(); UserStreamAddedSubscription?.Dispose(); UserStreamRemovedSubscription?.Dispose(); StreamUpdatedSubscription?.Dispose(); @@ -116,42 +103,34 @@ internal async Task ExecuteWithResiliencePolicies(Func> func) var delay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 5); var graphqlRetry = Policy - .Handle>() + .Handle() .WaitAndRetryAsync( delay, - (ex, timeout, context) => + (ex, timeout, _) => { - var graphqlEx = (SpeckleGraphQLException)ex; - SpeckleLog.Logger - .ForContext("graphqlExtensions", graphqlEx.Extensions) - .ForContext("graphqlErrorMessages", graphqlEx.ErrorMessages) - .Warning( - ex, - "The previous attempt at executing function to get {resultType} failed with {exceptionMessage}. Retrying after {timeout}.", - typeof(T).Name, - ex.Message, - timeout - ); + SpeckleLog.Logger.Debug( + ex, + "The previous attempt at executing function to get {resultType} failed with {exceptionMessage}. Retrying after {timeout}", + typeof(T).Name, + ex.Message, + timeout + ); } ); return await graphqlRetry.ExecuteAsync(func).ConfigureAwait(false); } - /// "FORBIDDEN" on "UNAUTHORIZED" response from server - /// All other request errors - /// The requested a cancel + /// public async Task ExecuteGraphQLRequest(GraphQLRequest request, CancellationToken cancellationToken = default) { using IDisposable context0 = LogContext.Push(CreateEnrichers(request)); + var timer = Stopwatch.StartNew(); - SpeckleLog.Logger.Debug("Starting execution of graphql request to get {resultType}", typeof(T).Name); - var timer = new Stopwatch(); - var success = false; - timer.Start(); + Exception? exception = null; try { - var result = await ExecuteWithResiliencePolicies(async () => + return await ExecuteWithResiliencePolicies(async () => { GraphQLResponse result = await GQLClient .SendMutationAsync(request, cancellationToken) @@ -160,58 +139,28 @@ public async Task ExecuteGraphQLRequest(GraphQLRequest request, Cancellati return result.Data; }) .ConfigureAwait(false); - success = true; - return result; } - // cancellations are bubbling up with no logging - catch (OperationCanceledException) + catch (Exception ex) { + exception = ex; throw; } - // we catch forbidden to rethrow, making sure its not logged. - catch (SpeckleGraphQLForbiddenException) - { - throw; - } - // anything else related to graphql gets logged - catch (SpeckleGraphQLException gqlException) - { - SpeckleLog.Logger - .ForContext("graphqlResponse", gqlException.Response) - .ForContext("graphqlExtensions", gqlException.Extensions) - .ForContext("graphqlErrorMessages", gqlException.ErrorMessages.ToList()) - .Warning( - gqlException, - "Execution of the graphql request to get {resultType} failed with {graphqlExceptionType} {exceptionMessage}.", - typeof(T).Name, - gqlException.GetType().Name, - gqlException.Message - ); - throw; - } - // we log and wrap anything that is not a graphql exception. - // this makes sure, that any graphql operation only throws SpeckleGraphQLExceptions - catch (Exception ex) when (!ex.IsFatal()) - { - SpeckleLog.Logger.Warning( - ex, - "Execution of the graphql request to get {resultType} failed without a graphql response. Cause {exceptionMessage}", - typeof(T).Name, - ex.Message - ); - throw new SpeckleGraphQLException("The graphql request failed without a graphql response", ex, request, null); - } finally { - // this is a performance metric log operation - // this makes sure that both success and failed operations report - // the same performance log - timer.Stop(); - var status = success ? "succeeded" : "failed"; - SpeckleLog.Logger.Information( - "Execution of graphql request to get {resultType} {resultStatus} after {elapsed} seconds", + LogEventLevel logLevel = exception switch + { + null => LogEventLevel.Information, + OperationCanceledException + => cancellationToken.IsCancellationRequested ? LogEventLevel.Debug : LogEventLevel.Error, + SpeckleException => LogEventLevel.Warning, + _ => LogEventLevel.Error, + }; + SpeckleLog.Logger.Write( + logLevel, + exception, + "Execution of the graphql request to get {resultType} completed with success:{status} after {elapsed} seconds", typeof(T).Name, - status, + exception is null, timer.Elapsed.TotalSeconds ); } @@ -236,7 +185,7 @@ internal void MaybeThrowFromGraphQLErrors(GraphQLRequest request, GraphQLResp ) ) { - throw new SpeckleGraphQLForbiddenException(request, response); + throw new SpeckleGraphQLForbiddenException(request, response); } if ( @@ -246,7 +195,7 @@ internal void MaybeThrowFromGraphQLErrors(GraphQLRequest request, GraphQLResp ) ) { - throw new SpeckleGraphQLStreamNotFoundException(request, response); + throw new SpeckleGraphQLStreamNotFoundException(request, response); } if ( @@ -257,7 +206,7 @@ internal void MaybeThrowFromGraphQLErrors(GraphQLRequest request, GraphQLResp ) ) { - throw new SpeckleGraphQLInternalErrorException(request, response); + throw new SpeckleGraphQLInternalErrorException(request, response); } throw new SpeckleGraphQLException("Request failed with errors", request, response); @@ -299,6 +248,10 @@ private ILogEventEnricher[] CreateEnrichers(GraphQLRequest request) }; } + IDisposable ISpeckleGraphQLClient.SubscribeTo(GraphQLRequest request, Action callback) => + SubscribeTo(request, callback); + + /// internal IDisposable SubscribeTo(GraphQLRequest request, Action callback) { using (LogContext.Push(CreateEnrichers(request))) @@ -325,7 +278,7 @@ internal IDisposable SubscribeTo(GraphQLRequest request, Action ca } } // we catch forbidden to rethrow, making sure its not logged. - catch (SpeckleGraphQLForbiddenException) + catch (SpeckleGraphQLForbiddenException) { throw; } @@ -356,7 +309,7 @@ internal IDisposable SubscribeTo(GraphQLRequest request, Action ca // so far we've swallowed these errors SpeckleLog.Logger.Error( ex, - "Subscription request for {resultType} failed with {exceptionMessage}", + "Subscription for {resultType} terminated unexpectedly with {exceptionMessage}", typeof(T).Name, ex.Message ); @@ -375,11 +328,57 @@ internal IDisposable SubscribeTo(GraphQLRequest request, Action ca ); throw new SpeckleGraphQLException( "The graphql request failed without a graphql response", - ex, request, - null + null, + ex ); } } } + + private static GraphQLHttpClient CreateGraphQLClient(Account account, HttpClient httpClient) + { + var gQLClient = new GraphQLHttpClient( + new GraphQLHttpClientOptions + { + EndPoint = new Uri(new Uri(account.serverInfo.url), "/graphql"), + UseWebSocketForQueriesAndMutations = false, + WebSocketProtocol = "graphql-ws", + ConfigureWebSocketConnectionInitPayload = _ => + { + return Http.CanAddAuth(account.token, out string? authValue) ? new { Authorization = authValue } : null; + }, + }, + new NewtonsoftJsonSerializer(), + httpClient + ); + + gQLClient.WebSocketReceiveErrors.Subscribe(e => + { + if (e is WebSocketException we) + { + Console.WriteLine( + $"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}" + ); + } + else + { + Console.WriteLine($"Exception in websocket receive stream: {e}"); + } + }); + return gQLClient; + } + + private static HttpClient CreateHttpClient(Account account) + { + var httpClient = Http.GetHttpProxyClient(null, TimeSpan.FromSeconds(30)); + Http.AddAuthHeader(httpClient, account.token); + + httpClient.DefaultRequestHeaders.Add("apollographql-client-name", Setup.HostApplication); + httpClient.DefaultRequestHeaders.Add( + "apollographql-client-version", + Assembly.GetExecutingAssembly().GetName().Version.ToString() + ); + return httpClient; + } } diff --git a/Core/Core/Api/GraphQL/Enums/FileUploadConversionStatus.cs b/Core/Core/Api/GraphQL/Enums/FileUploadConversionStatus.cs new file mode 100644 index 0000000000..4ab9e751bc --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/FileUploadConversionStatus.cs @@ -0,0 +1,10 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +//This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum) +public enum FileUploadConversionStatus +{ + Queued = 0, + Processing = 1, + Success = 2, + Error = 3, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs new file mode 100644 index 0000000000..2f11301dec --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs @@ -0,0 +1,8 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectCommentsUpdatedMessageType +{ + ARCHIVED, + CREATED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs new file mode 100644 index 0000000000..7cb7d933b9 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectFileImportUpdatedMessageType +{ + CREATED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs new file mode 100644 index 0000000000..1416691fa0 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs @@ -0,0 +1,8 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectModelsUpdatedMessageType +{ + CREATED, + DELETED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs new file mode 100644 index 0000000000..42ac2bebf1 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectPendingModelsUpdatedMessageType +{ + CREATED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs new file mode 100644 index 0000000000..3eccdd5a04 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectUpdatedMessageType +{ + DELETED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs new file mode 100644 index 0000000000..14e1f7008e --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs @@ -0,0 +1,8 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectVersionsUpdatedMessageType +{ + CREATED, + DELETED, + UPDATED, +} diff --git a/Core/Core/Api/GraphQL/Enums/ProjectVisibility.cs b/Core/Core/Api/GraphQL/Enums/ProjectVisibility.cs new file mode 100644 index 0000000000..9a62fff999 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ProjectVisibility.cs @@ -0,0 +1,8 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ProjectVisibility +{ + Private, + Public, + Unlisted +} diff --git a/Core/Core/Api/GraphQL/Enums/ResourceType.cs b/Core/Core/Api/GraphQL/Enums/ResourceType.cs new file mode 100644 index 0000000000..2fa31c46f1 --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/ResourceType.cs @@ -0,0 +1,9 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum ResourceType +{ + commit, + stream, + @object, + comment +} diff --git a/Core/Core/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs b/Core/Core/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs new file mode 100644 index 0000000000..5225929afe --- /dev/null +++ b/Core/Core/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Api.GraphQL.Enums; + +public enum UserProjectsUpdatedMessageType +{ + ADDED, + REMOVED, +} diff --git a/Core/Core/Api/GraphQL/GraphQLHttpClientExtensions.cs b/Core/Core/Api/GraphQL/GraphQLHttpClientExtensions.cs index 64b7a76645..0e7244e98b 100644 --- a/Core/Core/Api/GraphQL/GraphQLHttpClientExtensions.cs +++ b/Core/Core/Api/GraphQL/GraphQLHttpClientExtensions.cs @@ -3,6 +3,8 @@ using System.Threading; using GraphQL.Client.Http; using System.Linq; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; namespace Speckle.Core.Api.GraphQL; @@ -13,7 +15,7 @@ public static class GraphQLHttpClientExtensions /// /// [Optional] defaults to an empty cancellation token /// object excluding any strings (eg "2.7.2-alpha.6995" becomes "2.7.2.6995") - /// + /// public static async Task GetServerVersion( this GraphQLHttpClient client, CancellationToken cancellationToken = default diff --git a/Core/Core/Api/GraphQL/ISpeckleGraphQLClient.cs b/Core/Core/Api/GraphQL/ISpeckleGraphQLClient.cs new file mode 100644 index 0000000000..5b6a372095 --- /dev/null +++ b/Core/Core/Api/GraphQL/ISpeckleGraphQLClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using GraphQL; + +namespace Speckle.Core.Api.GraphQL; + +internal interface ISpeckleGraphQLClient +{ + /// "FORBIDDEN" on "UNAUTHORIZED" response from server + /// All other request errors + /// The requested a cancel + /// This already been disposed + internal Task ExecuteGraphQLRequest(GraphQLRequest request, CancellationToken cancellationToken); + + /// "FORBIDDEN" on "UNAUTHORIZED" response from server + /// All other request errors + /// This already been disposed + internal IDisposable SubscribeTo(GraphQLRequest request, Action callback); +} diff --git a/Core/Core/Api/GraphQL/Inputs/CommentInputs.cs b/Core/Core/Api/GraphQL/Inputs/CommentInputs.cs new file mode 100644 index 0000000000..df810adeb6 --- /dev/null +++ b/Core/Core/Api/GraphQL/Inputs/CommentInputs.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Inputs; + +public sealed record CreateCommentInput( + CommentContentInput content, + string projectId, + string resourceIdString, + string? screenshot, + object? viewerState +); + +public sealed record EditCommentInput(CommentContentInput content, string commentId); + +public sealed record CreateCommentReplyInput(CommentContentInput content, string threadId); + +public sealed record CommentContentInput(IReadOnlyCollection? blobIds, object? doc); diff --git a/Core/Core/Api/GraphQL/Inputs/ModelInputs.cs b/Core/Core/Api/GraphQL/Inputs/ModelInputs.cs new file mode 100644 index 0000000000..817df64cab --- /dev/null +++ b/Core/Core/Api/GraphQL/Inputs/ModelInputs.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Inputs; + +public sealed record CreateModelInput(string name, string? description, string projectId); + +public sealed record DeleteModelInput(string id, string projectId); + +public sealed record UpdateModelInput(string id, string? name, string? description, string projectId); + +public sealed record ModelVersionsFilter(IReadOnlyList priorityIds, bool? priorityIdsOnly); diff --git a/Core/Core/Api/GraphQL/Inputs/ProjectInputs.cs b/Core/Core/Api/GraphQL/Inputs/ProjectInputs.cs new file mode 100644 index 0000000000..568d0093aa --- /dev/null +++ b/Core/Core/Api/GraphQL/Inputs/ProjectInputs.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Speckle.Core.Api.GraphQL.Enums; + +namespace Speckle.Core.Api.GraphQL.Inputs; + +public sealed record ProjectCommentsFilter(bool? includeArchived, bool? loadedVersionsOnly, string? resourceIdString); + +public sealed record ProjectCreateInput(string? name, string? description, ProjectVisibility? visibility); + +public sealed record ProjectInviteCreateInput(string? email, string? role, string? serverRole, string? userId); + +public sealed record ProjectInviteUseInput(bool accept, string projectId, string token); + +public sealed record ProjectModelsFilter( + IReadOnlyList? contributors, + IReadOnlyList? excludeIds, + IReadOnlyList? ids, + bool? onlyWithVersions, + string? search, + IReadOnlyList sourceApps +); + +public sealed record ProjectModelsTreeFilter( + IReadOnlyList? contributors, + string? search, + IReadOnlyList? sourceApps +); + +public sealed record ProjectUpdateInput( + string id, + string? name = null, + string? description = null, + bool? allowPublicComments = null, + ProjectVisibility? visibility = null +); + +public sealed record ProjectUpdateRoleInput(string userId, string projectId, string? role); + +public sealed record UserProjectsFilter(string search, IReadOnlyList? onlyWithRoles = null); diff --git a/Core/Core/Api/GraphQL/Inputs/SubscriptionInputs.cs b/Core/Core/Api/GraphQL/Inputs/SubscriptionInputs.cs new file mode 100644 index 0000000000..86688a2864 --- /dev/null +++ b/Core/Core/Api/GraphQL/Inputs/SubscriptionInputs.cs @@ -0,0 +1,7 @@ +namespace Speckle.Core.Api.GraphQL.Inputs; + +public sealed record ViewerUpdateTrackingTarget( + string projectId, + string resourceIdString, + bool? loadedVersionsOnly = null +); diff --git a/Core/Core/Api/GraphQL/Inputs/VersionInputs.cs b/Core/Core/Api/GraphQL/Inputs/VersionInputs.cs new file mode 100644 index 0000000000..5bbee6e791 --- /dev/null +++ b/Core/Core/Api/GraphQL/Inputs/VersionInputs.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Inputs; + +public sealed record UpdateVersionInput(string versionId, string? message); + +public sealed record MoveVersionsInput(string targetModelName, IReadOnlyList versionIds); + +public sealed record DeleteVersionsInput(IReadOnlyList versionIds); diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ActivityOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ActivityOperations.cs similarity index 96% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ActivityOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ActivityOperations.cs index 34250ecb8a..df43029474 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ActivityOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ActivityOperations.cs @@ -8,6 +8,7 @@ namespace Speckle.Core.Api; public partial class Client { + //TODO: API gap /// /// Gets the activity of a stream /// @@ -25,7 +26,7 @@ public async Task> StreamGetActivity( DateTime? before = null, DateTime? cursor = null, string actionType = "", - int limit = 25, + int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST, CancellationToken cancellationToken = default ) { diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.BranchOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.BranchOperations.cs similarity index 86% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.BranchOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.BranchOperations.cs index 1c3a67b181..456098e093 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.BranchOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.BranchOperations.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GraphQL; +using Speckle.Core.Api.GraphQL.Resources; namespace Speckle.Core.Api; @@ -14,6 +16,9 @@ public partial class Client /// Id of the stream to get the branches from /// Max number of commits to retrieve /// + /// + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.GetModels)}")] public async Task> StreamGetBranchesWithLimitRetry(string streamId, int commitsLimit = 10) { List branches; @@ -38,6 +43,9 @@ public async Task> StreamGetBranchesWithLimitRetry(string streamId, /// Max number of commits to retrieve /// /// + /// + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.GetModels)}")] public async Task> StreamGetBranches( string streamId, int branchesLimit = 10, @@ -86,6 +94,8 @@ public async Task> StreamGetBranches( /// /// /// The branch id. + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.Create)}")] public async Task BranchCreate(BranchCreateInput branchInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -105,6 +115,10 @@ public async Task BranchCreate(BranchCreateInput branchInput, Cancellati /// Name of the branch to get /// /// The requested branch + /// Updated to Model.GetWithVersions + /// + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.Get)}")] public async Task BranchGet( string streamId, string branchName, @@ -154,6 +168,8 @@ public async Task BranchGet( /// Id of the project to get the model from /// Id of the model /// + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.Get)}")] public async Task ModelGet(string projectId, string modelId, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -190,6 +206,8 @@ public async Task ModelGet(string projectId, string modelId, Cancellatio /// /// /// The stream's id. + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.Update)}")] public async Task BranchUpdate(BranchUpdateInput branchInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -208,6 +226,8 @@ public async Task BranchUpdate(BranchUpdateInput branchInput, Cancellation /// /// /// + /// + [Obsolete($"Use client.{nameof(Model)}.{nameof(ModelResource.Delete)}")] public async Task BranchDelete(BranchDeleteInput branchInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommentOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommentOperations.cs similarity index 89% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommentOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommentOperations.cs index 20feaaf3be..aebce10af3 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommentOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommentOperations.cs @@ -1,6 +1,8 @@ +using System; using System.Threading; using System.Threading.Tasks; using GraphQL; +using Speckle.Core.Api.GraphQL.Resources; namespace Speckle.Core.Api; @@ -14,6 +16,8 @@ public partial class Client /// Time to filter the comments with /// /// + /// + [Obsolete($"Use client.{nameof(CommentResource)}.{nameof(CommentResource.GetProjectComments)}")] public async Task StreamGetComments( string streamId, int limit = 25, @@ -78,6 +82,8 @@ public async Task StreamGetComments( /// Id of the stream to get the comment from /// /// + /// + [Obsolete($"Use client.{nameof(CommentResource)}.{nameof(CommentResource.GetProjectComments)}")] public async Task StreamGetCommentScreenshot( string id, string streamId, diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommitOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommitOperations.cs similarity index 85% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommitOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommitOperations.cs index d76bc43037..d267b7623d 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.CommitOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.CommitOperations.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GraphQL; +using Speckle.Core.Api.GraphQL.Resources; namespace Speckle.Core.Api; @@ -14,6 +16,8 @@ public partial class Client /// Id of the commit to get /// /// + /// + [Obsolete($"Use client.{nameof(Version)}.{nameof(VersionResource.Get)}")] public async Task CommitGet(string streamId, string commitId, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -48,6 +52,8 @@ public async Task CommitGet(string streamId, string commitId, Cancellati /// Max number of commits to get /// /// The requested commits + /// + [Obsolete($"Use client.{nameof(Version)}.{nameof(VersionResource.GetVersions)}")] public async Task> StreamGetCommits( string streamId, int limit = 10, @@ -88,6 +94,8 @@ public async Task> StreamGetCommits( /// /// /// The commit id. + /// + [Obsolete($"Use client.{nameof(VersionResource)}.{nameof(VersionResource.Create)}")] public async Task CommitCreate(CommitCreateInput commitInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -106,6 +114,8 @@ public async Task CommitCreate(CommitCreateInput commitInput, Cancellati /// /// /// The stream's id. + /// + [Obsolete($"Use client.{nameof(VersionResource)}.{nameof(VersionResource.Update)}")] public async Task CommitUpdate(CommitUpdateInput commitInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -124,6 +134,8 @@ public async Task CommitUpdate(CommitUpdateInput commitInput, Cancellation /// /// /// + /// + [Obsolete($"Use client.{nameof(VersionResource)}.{nameof(VersionResource.Delete)}")] public async Task CommitDelete(CommitDeleteInput commitInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -143,6 +155,8 @@ public async Task CommitDelete(CommitDeleteInput commitInput, Cancellation /// /// /// + /// + [Obsolete($"Use client.{nameof(VersionResource)}.{nameof(VersionResource.Received)}")] public async Task CommitReceived( CommitReceivedInput commitReceivedInput, CancellationToken cancellationToken = default diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObjectOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ObjectOperations.cs similarity index 99% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObjectOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ObjectOperations.cs index 79e9f8b90f..0f842bc5e6 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ObjectOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ObjectOperations.cs @@ -6,6 +6,7 @@ namespace Speckle.Core.Api; public partial class Client { + //TODO: API Gap /// /// Gets data about the requested Speckle object from a stream. /// diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ServerOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ServerOperations.cs similarity index 96% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ServerOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ServerOperations.cs index 2244822d73..1ceb07fab1 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.ServerOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.ServerOperations.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using GraphQL; +using Speckle.Core.Api.GraphQL.Models.Responses; using Speckle.Core.Logging; namespace Speckle.Core.Api; diff --git a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.StreamOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.StreamOperations.cs similarity index 87% rename from Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.StreamOperations.cs rename to Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.StreamOperations.cs index f90a6849f3..dd7be354a3 100644 --- a/Core/Core/Api/GraphQL/Client.GraphqlCleintOperations/Client.StreamOperations.cs +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.StreamOperations.cs @@ -4,6 +4,9 @@ using System.Threading; using System.Threading.Tasks; using GraphQL; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; +using Speckle.Core.Api.GraphQL.Resources; using Speckle.Core.Logging; namespace Speckle.Core.Api; @@ -34,11 +37,11 @@ public async Task IsStreamAccessible(string id, CancellationToken cancella return stream.id == id; } - catch (SpeckleGraphQLForbiddenException) + catch (SpeckleGraphQLForbiddenException) { return false; } - catch (SpeckleGraphQLStreamNotFoundException) + catch (SpeckleGraphQLStreamNotFoundException) { return false; } @@ -52,6 +55,9 @@ public async Task IsStreamAccessible(string id, CancellationToken cancella /// Max number of branches to retrieve /// /// + /// + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.GetWithModels)}")] public async Task StreamGet(string id, int branchesLimit = 10, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -100,6 +106,8 @@ public async Task StreamGet(string id, int branchesLimit = 10, Cancellat /// Max number of streams to return /// /// + /// + [Obsolete($"Use client.{nameof(ActiveUser)}.{nameof(ActiveUserResource.GetProjects)}")] public async Task> StreamsGet(int limit = 10, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -142,7 +150,7 @@ public async Task> StreamsGet(int limit = 10, CancellationToken can }}" }; - var res = await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false); + var res = await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false); if (res?.activeUser == null) { @@ -154,6 +162,7 @@ public async Task> StreamsGet(int limit = 10, CancellationToken can return res.activeUser.streams.items; } + //TODO: API GAP /// /// Gets all favorite streams for the current user /// @@ -201,7 +210,7 @@ public async Task> FavoriteStreamsGet(int limit = 10, CancellationT }} }}" }; - return (await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false)) + return (await ExecuteGraphQLRequest(request, cancellationToken).ConfigureAwait(false)) .activeUser .favoriteStreams .items; @@ -214,6 +223,8 @@ public async Task> FavoriteStreamsGet(int limit = 10, CancellationT /// Max number of streams to return /// /// + /// + [Obsolete($"Use client.{nameof(ActiveUser)}.{nameof(ActiveUserResource.GetProjects)}")] public async Task> StreamSearch( string query, int limit = 10, @@ -258,6 +269,8 @@ public async Task> StreamSearch( /// /// /// The stream's id. + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.Create)}")] public async Task StreamCreate(StreamCreateInput streamInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -275,6 +288,8 @@ public async Task StreamCreate(StreamCreateInput streamInput, Cancellati /// Note: the id field needs to be a valid stream id. /// /// The stream's id. + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.Update)}")] public async Task StreamUpdate(StreamUpdateInput streamInput, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -294,6 +309,8 @@ public async Task StreamUpdate(StreamUpdateInput streamInput, Cancellation /// Id of the stream to be deleted /// /// + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.Delete)}")] public async Task StreamDelete(string id, CancellationToken cancellationToken = default) { var request = new GraphQLRequest @@ -336,6 +353,8 @@ public async Task StreamRevokePermission( /// /// /// + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.UpdateRole)}")] public async Task StreamUpdatePermission( StreamPermissionInput updatePermissionInput, CancellationToken cancellationToken = default @@ -362,6 +381,8 @@ mutation streamUpdatePermission($permissionParams: StreamUpdatePermissionInput!) /// /// /// + /// + [Obsolete($"Use client.{nameof(Project)}.{nameof(ProjectResource.GetWithTeam)}")] public async Task StreamGetPendingCollaborators( string streamId, CancellationToken cancellationToken = default @@ -396,6 +417,8 @@ public async Task StreamGetPendingCollaborators( /// /// /// + /// + [Obsolete($"Use client.{nameof(ProjectInvite)}.{nameof(ProjectInviteResource.Create)}")] public async Task StreamInviteCreate( StreamInviteCreateInput inviteCreateInput, CancellationToken cancellationToken = default @@ -427,6 +450,8 @@ mutation streamInviteCreate($input: StreamInviteCreateInput!) { /// Id of the invite to cancel /// /// + /// + [Obsolete($"Use client.{nameof(ProjectInvite)}.{nameof(ProjectInviteResource.Cancel)}")] public async Task StreamInviteCancel( string streamId, string inviteId, @@ -456,6 +481,8 @@ mutation streamInviteCancel( $streamId: String!, $inviteId: String! ) { /// /// /// + /// + [Obsolete($"Use client.{nameof(ProjectInvite)}.{nameof(ProjectInviteResource.Use)}")] public async Task StreamInviteUse( string streamId, string token, @@ -482,6 +509,13 @@ mutation streamInviteUse( $accept: Boolean!, $streamId: String!, $token: String! return (bool)res["streamInviteUse"]; } + /// + /// + /// + /// + /// + /// + [Obsolete($"Use client.{nameof(ActiveUser)}.{nameof(ActiveUserResource.ProjectInvites)}")] public async Task> GetAllPendingInvites(CancellationToken cancellationToken = default) { var request = new GraphQLRequest diff --git a/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.UserOperations.cs b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.UserOperations.cs new file mode 100644 index 0000000000..b1cbd1e762 --- /dev/null +++ b/Core/Core/Api/GraphQL/Legacy/Client.GraphqlCleintOperations/Client.UserOperations.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Api; + +public partial class Client +{ + /// + /// Gets the currently active user profile. + /// + /// + /// + /// + [Obsolete($"Use client.{nameof(ActiveUser)}.{nameof(ActiveUserResource.Get)}")] + public async Task ActiveUserGet(CancellationToken cancellationToken = default) + { + return await ActiveUser.Get(cancellationToken).ConfigureAwait(false); + } + + /// + /// Get another user's profile by its user id. + /// + /// Id of the user you are looking for + /// + /// + /// + [Obsolete($"Use client.{nameof(OtherUser)}.{nameof(OtherUserResource.Get)}")] + public async Task OtherUserGet(string id, CancellationToken cancellationToken = default) + { + return await OtherUser.Get(id, cancellationToken).ConfigureAwait(false); + } + + /// + /// Searches for a user on the server. + /// + /// String to search for. Must be at least 3 characters + /// Max number of users to return + /// + /// + [Obsolete($"Use client.{nameof(OtherUser)}.{nameof(OtherUserResource.UserSearch)}")] + public async Task> UserSearch( + string query, + int limit = 10, + CancellationToken cancellationToken = default + ) + { + var res = await OtherUser.UserSearch(query, limit, cancellationToken: cancellationToken).ConfigureAwait(false); + return res.items; + } +} diff --git a/Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Branch.cs b/Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Branch.cs similarity index 100% rename from Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Branch.cs rename to Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Branch.cs diff --git a/Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Commit.cs b/Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Commit.cs similarity index 100% rename from Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Commit.cs rename to Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Commit.cs diff --git a/Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Stream.cs b/Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Stream.cs similarity index 100% rename from Core/Core/Api/GraphQL/Client.Subscriptions/Client.Subscriptions.Stream.cs rename to Core/Core/Api/GraphQL/Legacy/Client.Subscriptions/Client.Subscriptions.Stream.cs diff --git a/Core/Core/Api/GraphQL/Models.cs b/Core/Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs similarity index 59% rename from Core/Core/Api/GraphQL/Models.cs rename to Core/Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs index a38d758d38..5dcff259f6 100644 --- a/Core/Core/Api/GraphQL/Models.cs +++ b/Core/Core/Api/GraphQL/Legacy/LegacyGraphQLModels.cs @@ -1,13 +1,20 @@ #nullable disable using System; using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text.Json.Serialization; +using Speckle.Core.Api.GraphQL.Enums; +using Speckle.Core.Api.GraphQL.Models; namespace Speckle.Core.Api; #region inputs +internal static class DeprecationMessages +{ + public const string FE2_DEPRECATION_MESSAGE = + $"Stream/Branch/Commit API is now deprecated, Use the new Project/Model/Version API functions in {nameof(Client)}"; +} + +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamCreateInput { public string name { get; set; } @@ -15,6 +22,7 @@ public class StreamCreateInput public bool isPublic { get; set; } = true; } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamUpdateInput { public string id { get; set; } @@ -23,6 +31,7 @@ public class StreamUpdateInput public bool isPublic { get; set; } = true; } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamPermissionInput { public string streamId { get; set; } @@ -30,12 +39,14 @@ public class StreamPermissionInput public string role { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamRevokePermissionInput { public string streamId { get; set; } public string userId { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamInviteCreateInput { public string streamId { get; set; } @@ -45,6 +56,7 @@ public class StreamInviteCreateInput public string role { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchCreateInput { public string streamId { get; set; } @@ -52,6 +64,7 @@ public class BranchCreateInput public string description { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchUpdateInput { public string streamId { get; set; } @@ -60,6 +73,7 @@ public class BranchUpdateInput public string description { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchDeleteInput { public string streamId { get; set; } @@ -80,6 +94,7 @@ public class CommitCreateInput public List previousCommitIds { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitUpdateInput { public string streamId { get; set; } @@ -87,12 +102,14 @@ public class CommitUpdateInput public string message { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitDeleteInput { public string streamId { get; set; } public string id { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitReceivedInput { public string streamId { get; set; } @@ -103,6 +120,7 @@ public class CommitReceivedInput #endregion +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Stream { public string id { get; set; } @@ -147,6 +165,7 @@ public override string ToString() } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Collaborator { public string id { get; set; } @@ -160,24 +179,13 @@ public override string ToString() } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamInvitesResponse { public List streamInvites { get; set; } } -public class PendingStreamCollaborator -{ - public string id { get; set; } - public string inviteId { get; set; } - public string streamId { get; set; } - public string streamName { get; set; } - public string title { get; set; } - public string role { get; set; } - public User invitedBy { get; set; } - public User user { get; set; } - public string token { get; set; } -} - +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Branches { public int totalCount { get; set; } @@ -185,6 +193,7 @@ public class Branches public List items { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Commits { public int totalCount { get; set; } @@ -192,6 +201,7 @@ public class Commits public List items { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Commit { public string id { get; set; } @@ -247,6 +257,7 @@ public class InfoCommit public string branchName { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class SpeckleObject { public string id { get; set; } @@ -256,6 +267,7 @@ public class SpeckleObject public DateTime createdAt { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Branch { public string id { get; set; } @@ -269,6 +281,7 @@ public override string ToString() } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Streams { public int totalCount { get; set; } @@ -276,51 +289,14 @@ public class Streams public List items { get; set; } } -public class UserBase -{ - public string id { get; set; } - public string name { get; set; } - public string bio { get; set; } - public string company { get; set; } - public string avatar { get; set; } - public bool verified { get; set; } - public string role { get; set; } - public Streams streams { get; set; } -} - -public class LimitedUser : UserBase -{ - public override string ToString() - { - return $"Other user profile: ({name} | {id})"; - } -} - -public class User : UserBase -{ - public string email { get; set; } - public Streams favoriteStreams { get; set; } - - public override string ToString() - { - return $"User ({email} | {name} | {id})"; - } -} - +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Resource { public string resourceId { get; set; } public ResourceType resourceType { get; set; } } -public enum ResourceType -{ - commit, - stream, - @object, - comment -} - +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Location { public double x { get; set; } @@ -328,105 +304,33 @@ public class Location public double z { get; set; } } -public class UserData -{ - public User user { get; set; } -} - -/// -/// GraphQL DTO model for active user data -/// -public class ActiveUserData -{ - /// - /// User profile of the active user. - /// - public User activeUser { get; set; } -} - -/// -/// GraphQL DTO model for limited user data. Mostly referring to other user's profile. -/// -public class LimitedUserData -{ - /// - /// The limited user profile of another (non active user) - /// - public LimitedUser otherUser { get; set; } -} - +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class UserSearchData { public UserSearch userSearch { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class UserSearch { public string cursor { get; set; } public List items { get; set; } } -public class ServerInfoResponse -{ - // TODO: server and user models are duplicated here and in Core.Credentials.Responses - // a bit weird and unnecessary - shouldn't both Credentials and Api share the same models since they're - // all server models that should be consistent? am creating a new obj here as to not reference Credentials in - // this file but it should prob be refactored in the futrue - public ServerInfo serverInfo { get; set; } -} - -// TODO: prob remove and bring one level up and shared w Core.Credentials -[ClassInterface(ClassInterfaceType.AutoDual)] -[ComVisible(true)] -public class ServerInfo -{ - public string name { get; set; } - public string company { get; set; } - public string version { get; set; } - public string adminContact { get; set; } - public string description { get; set; } - - /// - /// This field is not returned from the GQL API, - /// it should populated on construction from the response headers. - /// see - /// - public bool frontend2 { get; set; } - - /// - /// This field is not returned from the GQL API, - /// it should populated on construction. - /// see - /// - public string url { get; set; } - - public ServerMigration migration { get; set; } -} - -public class ServerMigration -{ - /// - /// New URI where this server is now deployed - /// - public Uri movedTo { get; set; } - - /// - /// Previous URI where this server used to be deployed - /// - public Uri movedFrom { get; set; } -} - +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamData { public Stream stream { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamsData { public Streams streams { get; set; } } #region comments +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Comments { public int totalCount { get; set; } @@ -434,16 +338,18 @@ public class Comments public List items { get; set; } } -public class CommentData +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] +public sealed class CommentData { - public Comments comments { get; set; } - public List camPos { get; set; } - public object filters { get; set; } - public Location location { get; set; } - public object selection { get; set; } - public object sectionBox { get; set; } + public Comments comments { get; init; } + public List camPos { get; init; } + public object filters { get; init; } + public Location location { get; init; } + public object selection { get; init; } + public object sectionBox { get; init; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommentItem { public string id { get; set; } @@ -460,6 +366,7 @@ public class CommentItem public List resources { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class ContentContent { public string Type { get; set; } @@ -468,116 +375,28 @@ public class ContentContent public string Text { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommentsData { public Comments comments { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommentItemData { public CommentItem comment { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommentActivityMessage { public string type { get; set; } public CommentItem comment { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommentActivityResponse { public CommentActivityMessage commentActivity { get; set; } } #endregion - -#region manager api - -public class Connector -{ - public List Versions { get; set; } = new(); -} - -public class Version -{ - public Version(string number, string url, Os os = Os.Win, Architecture architecture = Architecture.Any) - { - Number = number; - Url = url; - Date = DateTime.Now; - Prerelease = Number.Contains("-"); - Os = os; - Architecture = architecture; - } - - public string Number { get; set; } - public string Url { get; set; } - public Os Os { get; set; } - public Architecture Architecture { get; set; } = Architecture.Any; - public DateTime Date { get; set; } - - [JsonIgnore] - public string DateTimeAgo => Helpers.TimeAgo(Date); - - public bool Prerelease { get; set; } -} - -/// -/// OS -/// NOTE: do not edit order and only append new items as they are serialized to ints -/// -public enum Os -{ - Win, //0 - OSX, //1 - Linux, //2 - Any //3 -} - -/// -/// Architecture -/// NOTE: do not edit order and only append new items as they are serialized to ints -/// -public enum Architecture -{ - Any, //0 - Arm, //1 - Intel //2 -} - -//GHOST API -public class Meta -{ - public Pagination pagination { get; set; } -} - -public class Pagination -{ - public int page { get; set; } - public string limit { get; set; } - public int pages { get; set; } - public int total { get; set; } - public object next { get; set; } - public object prev { get; set; } -} - -public class Tags -{ - public List tags { get; set; } - public Meta meta { get; set; } -} - -public class Tag -{ - public string id { get; set; } - public string name { get; set; } - public string slug { get; set; } - public string description { get; set; } - public string feature_image { get; set; } - public string visibility { get; set; } - public string codeinjection_head { get; set; } - public object codeinjection_foot { get; set; } - public object canonical_url { get; set; } - public string accent_color { get; set; } - public string url { get; set; } -} -#endregion diff --git a/Core/Core/Api/GraphQL/Legacy/Manager.cs b/Core/Core/Api/GraphQL/Legacy/Manager.cs new file mode 100644 index 0000000000..94c1585efb --- /dev/null +++ b/Core/Core/Api/GraphQL/Legacy/Manager.cs @@ -0,0 +1,98 @@ +#nullable disable +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Speckle.Core.Api.GraphQL; + +#region manager api + +public class Connector +{ + public List Versions { get; set; } = new(); +} + +public class ConnectorVersion +{ + public ConnectorVersion(string number, string url, Os os = Os.Win, Architecture architecture = Architecture.Any) + { + Number = number; + Url = url; + Date = DateTime.Now; + Prerelease = Number.Contains("-"); + Os = os; + Architecture = architecture; + } + + public string Number { get; set; } + public string Url { get; set; } + public Os Os { get; set; } + public Architecture Architecture { get; set; } = Architecture.Any; + public DateTime Date { get; set; } + + [JsonIgnore] + public string DateTimeAgo => Helpers.TimeAgo(Date); + + public bool Prerelease { get; set; } +} + +/// +/// OS +/// NOTE: do not edit order and only append new items as they are serialized to ints +/// +public enum Os +{ + Win, //0 + OSX, //1 + Linux, //2 + Any //3 +} + +/// +/// Architecture +/// NOTE: do not edit order and only append new items as they are serialized to ints +/// +public enum Architecture +{ + Any, //0 + Arm, //1 + Intel //2 +} + +//GHOST API +public class Meta +{ + public Pagination pagination { get; set; } +} + +public class Pagination +{ + public int page { get; set; } + public string limit { get; set; } + public int pages { get; set; } + public int total { get; set; } + public object next { get; set; } + public object prev { get; set; } +} + +public class Tags +{ + public List tags { get; set; } + public Meta meta { get; set; } +} + +public class Tag +{ + public string id { get; set; } + public string name { get; set; } + public string slug { get; set; } + public string description { get; set; } + public string feature_image { get; set; } + public string visibility { get; set; } + public string codeinjection_head { get; set; } + public object codeinjection_foot { get; set; } + public object canonical_url { get; set; } + public string accent_color { get; set; } + public string url { get; set; } +} +#endregion diff --git a/Core/Core/Api/GraphQL/SubscriptionModels.cs b/Core/Core/Api/GraphQL/Legacy/SubscriptionModels.cs similarity index 74% rename from Core/Core/Api/GraphQL/SubscriptionModels.cs rename to Core/Core/Api/GraphQL/Legacy/SubscriptionModels.cs index f1b253b610..f330899f23 100644 --- a/Core/Core/Api/GraphQL/SubscriptionModels.cs +++ b/Core/Core/Api/GraphQL/Legacy/SubscriptionModels.cs @@ -5,6 +5,7 @@ namespace Speckle.Core.Api.SubscriptionModels; #region streams +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamInfo { public string id { get; set; } @@ -13,16 +14,19 @@ public class StreamInfo public string sharedBy { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class UserStreamAddedResult { public StreamInfo userStreamAdded { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class StreamUpdatedResult { public StreamInfo streamUpdated { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class UserStreamRemovedResult { public StreamInfo userStreamRemoved { get; set; } @@ -31,6 +35,7 @@ public class UserStreamRemovedResult #region branches +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchInfo { public string id { get; set; } @@ -40,16 +45,19 @@ public class BranchInfo public string authorId { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchCreatedResult { public BranchInfo branchCreated { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchUpdatedResult { public BranchInfo branchUpdated { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class BranchDeletedResult { public BranchInfo branchDeleted { get; set; } @@ -58,6 +66,7 @@ public class BranchDeletedResult #region commits +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitInfo { public string id { get; set; } @@ -74,16 +83,19 @@ public class CommitInfo public IList previousCommitIds { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitCreatedResult { public CommitInfo commitCreated { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitUpdatedResult { public CommitInfo commitUpdated { get; set; } } +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class CommitDeletedResult { public CommitInfo commitDeleted { get; set; } diff --git a/Core/Core/Api/GraphQL/Models/Collections.cs b/Core/Core/Api/GraphQL/Models/Collections.cs new file mode 100644 index 0000000000..3e4738aad0 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Collections.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public class ResourceCollection +{ + public int totalCount { get; init; } + + public List items { get; init; } + + public string? cursor { get; init; } +} + +public sealed class CommentReplyAuthorCollection : ResourceCollection { } + +public sealed class ProjectCommentCollection : ResourceCollection +{ + public int totalArchivedCount { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/Comment.cs b/Core/Core/Api/GraphQL/Models/Comment.cs new file mode 100644 index 0000000000..75da443dd3 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Comment.cs @@ -0,0 +1,25 @@ +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class Comment +{ + public bool archived { get; init; } + public LimitedUser author { get; init; } + public string authorId { get; init; } + public DateTime createdAt { get; init; } + public bool hasParent { get; init; } + public string id { get; init; } + public Comment parent { get; init; } + public string rawText { get; init; } + public ResourceCollection replies { get; init; } + public CommentReplyAuthorCollection replyAuthors { get; init; } + public List resources { get; init; } + public string screenshot { get; init; } + public DateTime updatedAt { get; init; } + public DateTime? viewedAt { get; init; } + public List viewerResources { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/FileUpload.cs b/Core/Core/Api/GraphQL/Models/FileUpload.cs new file mode 100644 index 0000000000..8327e24817 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/FileUpload.cs @@ -0,0 +1,30 @@ +#nullable disable + +using System; +using Speckle.Core.Api.GraphQL.Enums; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class FileUpload +{ + public string convertedCommitId { get; init; } + public DateTime convertedLastUpdate { get; init; } + public FileUploadConversionStatus convertedStatus { get; init; } + public string convertedVersionId { get; init; } + public string fileName { get; init; } + public int fileSize { get; init; } + public string fileType { get; init; } + public string id { get; init; } + public Model model { get; init; } + public string modelName { get; init; } + public string projectId { get; init; } + public bool uploadComplete { get; init; } + public DateTime uploadDate { get; init; } + public string userId { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public string branchName { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public string streamId { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/Model.cs b/Core/Core/Api/GraphQL/Models/Model.cs new file mode 100644 index 0000000000..3c779960fa --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Model.cs @@ -0,0 +1,23 @@ +#nullable disable +using System; + +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class Model +{ + public LimitedUser author { get; init; } + public List childrenTree { get; init; } + public ResourceCollection commentThreads { get; init; } + public DateTime createdAt { get; init; } + public string description { get; init; } + public string displayName { get; init; } + public string id { get; init; } + public string name { get; init; } + public List pendingImportedVersions { get; init; } + public Uri previewUrl { get; init; } + public DateTime updatedAt { get; init; } + public ResourceCollection versions { get; init; } + public Version version { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/ModelsTreeItem.cs b/Core/Core/Api/GraphQL/Models/ModelsTreeItem.cs new file mode 100644 index 0000000000..f0d6e49966 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ModelsTreeItem.cs @@ -0,0 +1,17 @@ +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class ModelsTreeItem +{ + public List children { get; init; } + public string fullName { get; init; } + public bool hasChildren { get; init; } + public string id { get; init; } + public Model model { get; init; } + public string name { get; init; } + public DateTime updatedAt { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/PendingStreamCollaborator.cs b/Core/Core/Api/GraphQL/Models/PendingStreamCollaborator.cs new file mode 100644 index 0000000000..805c0231ba --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/PendingStreamCollaborator.cs @@ -0,0 +1,25 @@ +#nullable disable +using System; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class PendingStreamCollaborator +{ + public string id { get; init; } + public string inviteId { get; init; } + + public string projectId { get; init; } + + public string projectName { get; init; } + public string title { get; init; } + public string role { get; init; } + public LimitedUser invitedBy { get; init; } + public LimitedUser user { get; init; } + public string token { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public string streamId { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public string streamName { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/Project.cs b/Core/Core/Api/GraphQL/Models/Project.cs new file mode 100644 index 0000000000..537ceb4d75 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Project.cs @@ -0,0 +1,30 @@ +#nullable disable +using System; +using System.Collections.Generic; +using Speckle.Core.Api.GraphQL.Enums; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class Project +{ + public bool AllowPublicComments { get; init; } + public ProjectCommentCollection commentThreads { get; init; } + public DateTime createdAt { get; init; } + public string description { get; init; } + public string id { get; init; } + public List invitedTeam { get; init; } + public ResourceCollection models { get; init; } + public string name { get; init; } + public List pendingImportedModels { get; init; } + public string role { get; init; } + public List sourceApps { get; init; } + public List team { get; init; } + public DateTime updatedAt { get; init; } + public ProjectVisibility visibility { get; init; } + + public List viewerResources { get; init; } + public ResourceCollection versions { get; init; } + public Model model { get; init; } + public List modelChildrenTree { get; init; } + public ResourceCollection modelsTree { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/ProjectCollaborator.cs b/Core/Core/Api/GraphQL/Models/ProjectCollaborator.cs new file mode 100644 index 0000000000..ea0628316a --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ProjectCollaborator.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class ProjectCollaborator +{ + public string role { get; init; } + public LimitedUser user { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/ResourceIdentifier.cs b/Core/Core/Api/GraphQL/Models/ResourceIdentifier.cs new file mode 100644 index 0000000000..670111caf9 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ResourceIdentifier.cs @@ -0,0 +1,10 @@ +#nullable disable +using Speckle.Core.Api.GraphQL.Enums; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class ResourceIdentifier +{ + public string resourceId { get; init; } + public ResourceType resourceType { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/Responses/MutationResponses.cs b/Core/Core/Api/GraphQL/Models/Responses/MutationResponses.cs new file mode 100644 index 0000000000..b456d05ea0 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Responses/MutationResponses.cs @@ -0,0 +1,42 @@ +namespace Speckle.Core.Api.GraphQL.Models.Responses; + +#nullable disable +internal sealed class ProjectMutation +{ + public Project create { get; init; } + public Project update { get; init; } + public bool delete { get; init; } + public ProjectInviteMutation invites { get; init; } + + public Project updateRole { get; init; } +} + +internal sealed class ProjectInviteMutation +{ + public Project create { get; init; } + public bool use { get; init; } + public Project cancel { get; init; } +} + +internal sealed class ModelMutation +{ + public Model create { get; init; } + public Model update { get; init; } + public bool delete { get; init; } +} + +internal sealed class VersionMutation +{ + public bool delete { get; init; } + public Model moveToModel { get; init; } + public Version update { get; init; } +} + +internal sealed class CommentMutation +{ + public bool archive { get; init; } + public Comment create { get; init; } + public Comment edit { get; init; } + public bool markViewed { get; init; } + public Comment reply { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/Responses/Responses.cs b/Core/Core/Api/GraphQL/Models/Responses/Responses.cs new file mode 100644 index 0000000000..e731f3118d --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Responses/Responses.cs @@ -0,0 +1,30 @@ +using Speckle.Newtonsoft.Json; + +namespace Speckle.Core.Api.GraphQL.Models.Responses; + +// This file holds simple records that represent the root GraphQL response data +// For this reason, we're keeping them internal, allowing us to be flexible without the concern for breaking. +// It also exposes fewer similarly named types to dependent assemblies + +internal record ProjectResponse([property: JsonRequired] Project project); + +internal record ActiveUserResponse(User? activeUser); + +internal record LimitedUserResponse(LimitedUser? otherUser); + +internal record ServerInfoResponse([property: JsonRequired] ServerInfo serverInfo); + +internal record ProjectMutationResponse([property: JsonRequired] ProjectMutation projectMutations); + +internal record ModelMutationResponse([property: JsonRequired] ModelMutation modelMutations); + +internal record VersionMutationResponse([property: JsonRequired] VersionMutation versionMutations); + +internal record ProjectInviteResponse(PendingStreamCollaborator? projectInvite); + +internal record UserSearchResponse([property: JsonRequired] ResourceCollection userSearch); + +//All of the above records could be replaced by either RequiredResponse or OptionalResponse, if we use an alias (see https://www.baeldung.com/graphql-field-name) +internal record RequiredResponse([property: JsonRequired] T data); + +internal record OptionalResponse(T? data); diff --git a/Core/Core/Api/GraphQL/Models/ServerInfo.cs b/Core/Core/Api/GraphQL/Models/ServerInfo.cs new file mode 100644 index 0000000000..c0c36a5a6f --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ServerInfo.cs @@ -0,0 +1,45 @@ +#nullable disable +using System; +using System.Runtime.InteropServices; + +namespace Speckle.Core.Api.GraphQL.Models; + +[ClassInterface(ClassInterfaceType.AutoDual)] +[ComVisible(true)] +public sealed class ServerInfo +{ + public string name { get; init; } + public string company { get; init; } + public string version { get; init; } + public string adminContact { get; init; } + public string description { get; init; } + + /// + /// This field is not returned from the GQL API, + /// it should be populated after construction from the response headers. + /// see + /// + public bool frontend2 { get; set; } + + /// + /// This field is not returned from the GQL API, + /// it should be populated after construction. + /// see + /// + public string url { get; set; } + + public ServerMigration migration { get; init; } +} + +public sealed class ServerMigration +{ + /// + /// New URI where this server is now deployed + /// + public Uri movedTo { get; set; } + + /// + /// Previous URI where this server used to be deployed + /// + public Uri movedFrom { get; set; } +} diff --git a/Core/Core/Api/GraphQL/Models/SubscriptionMessages.cs b/Core/Core/Api/GraphQL/Models/SubscriptionMessages.cs new file mode 100644 index 0000000000..0ce6dc5607 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/SubscriptionMessages.cs @@ -0,0 +1,84 @@ +using System; +using Speckle.Core.Api.GraphQL.Enums; +using Speckle.Newtonsoft.Json; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class UserProjectsUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public UserProjectsUpdatedMessageType type { get; init; } + + public Project? project { get; init; } +} + +public sealed class ProjectCommentsUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectCommentsUpdatedMessageType type { get; init; } + + public Comment? comment { get; init; } +} + +public sealed class ProjectFileImportUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectFileImportUpdatedMessageType type { get; init; } + + public FileUpload? upload { get; init; } +} + +public sealed class ProjectModelsUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectModelsUpdatedMessageType type { get; init; } + + public Model? model { get; init; } +} + +public sealed class ProjectPendingModelsUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectPendingModelsUpdatedMessageType type { get; init; } + + public FileUpload? model { get; init; } +} + +public sealed class ProjectUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectUpdatedMessageType type { get; init; } + + public Project? project { get; init; } +} + +public sealed class ProjectVersionsUpdatedMessage : EventArgs +{ + [JsonRequired] + public string id { get; init; } + + [JsonRequired] + public ProjectVersionsUpdatedMessageType type { get; init; } + + public string? modelId { get; init; } + + public Version? version { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/User.cs b/Core/Core/Api/GraphQL/Models/User.cs new file mode 100644 index 0000000000..f3d6b26863 --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/User.cs @@ -0,0 +1,52 @@ +#nullable disable +using System; +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public abstract class UserBase +{ + public ResourceCollection activity { get; init; } + public string avatar { get; init; } + public string bio { get; init; } + public string company { get; set; } + public string id { get; init; } + public string name { get; init; } + public string role { get; init; } + + public ResourceCollection timeline { get; init; } + public int totalOwnedStreamsFavorites { get; init; } + public bool? verified { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public ResourceCollection commits { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public ResourceCollection streams { get; init; } +} + +public sealed class LimitedUser : UserBase +{ + public override string ToString() + { + return $"Other user profile: ({name} | {id})"; + } +} + +public sealed class User : UserBase +{ + public DateTime? createdAt { get; init; } + public string email { get; init; } + public bool? hasPendingVerification { get; init; } + public bool? isOnboardingFinished { get; init; } + public List projectInvites { get; init; } + public ResourceCollection projects { get; init; } + + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public ResourceCollection favoriteStreams { get; init; } + + public override string ToString() + { + return $"User ({email} | {name} | {id})"; + } +} diff --git a/Core/Core/Api/GraphQL/Models/Version.cs b/Core/Core/Api/GraphQL/Models/Version.cs new file mode 100644 index 0000000000..1aa46b0dae --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/Version.cs @@ -0,0 +1,18 @@ +#nullable disable + +using System; + +namespace Speckle.Core.Api.GraphQL.Models; + +public sealed class Version +{ + public LimitedUser authorUser { get; init; } + public ResourceCollection commentThreads { get; init; } + public DateTime createdAt { get; init; } + public string id { get; init; } + public string message { get; init; } + public Model model { get; init; } + public Uri previewUrl { get; init; } + public string referencedObject { get; init; } + public string sourceApplication { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/ViewerResourceGroup.cs b/Core/Core/Api/GraphQL/Models/ViewerResourceGroup.cs new file mode 100644 index 0000000000..3ef11ed67f --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ViewerResourceGroup.cs @@ -0,0 +1,11 @@ +#nullable disable + +using System.Collections.Generic; + +namespace Speckle.Core.Api.GraphQL.Models; + +public class ViewerResourceGroup +{ + public string identifier { get; init; } + public List items { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Models/ViewerResourceItem.cs b/Core/Core/Api/GraphQL/Models/ViewerResourceItem.cs new file mode 100644 index 0000000000..12fc34e29f --- /dev/null +++ b/Core/Core/Api/GraphQL/Models/ViewerResourceItem.cs @@ -0,0 +1,10 @@ +#nullable disable + +namespace Speckle.Core.Api.GraphQL.Models; + +public class ViewerResourceItem +{ + public string modelId { get; init; } + public string objectId { get; init; } + public string versionId { get; init; } +} diff --git a/Core/Core/Api/GraphQL/Resources/ActiveUserResource.cs b/Core/Core/Api/GraphQL/Resources/ActiveUserResource.cs new file mode 100644 index 0000000000..400385cc93 --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/ActiveUserResource.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class ActiveUserResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal ActiveUserResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// Gets the currently active user profile. + /// + /// + /// + /// the requested user, or null if the user does not exist (i.e. was initialised with an unauthenticated account) + /// + public async Task Get(CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query User { + activeUser { + id, + email, + name, + bio, + company, + avatar, + verified, + profiles, + role, + } + } + """; + var request = new GraphQLRequest { Query = QUERY }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.activeUser; + } + + /// Max number of projects to fetch + /// Optional cursor for pagination + /// Optional filter + /// + /// + /// + public async Task> GetProjects( + 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) { + activeUser { + projects(limit: $limit, cursor: $cursor, filter: $filter) { + totalCount + items { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + } + } + } + } + """; + var request = new GraphQLRequest + { + Query = QUERY, + Variables = new + { + limit, + cursor, + filter + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + if (response.activeUser is null) + { + throw new SpeckleGraphQLException("GraphQL response indicated that the ActiveUser could not be found"); + } + + return response.activeUser.projects; + } + + /// + /// + /// + public async Task> ProjectInvites(CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query ProjectInvites { + activeUser { + projectInvites { + id + inviteId + invitedBy { + avatar + bio + company + id + name + role + verified + } + projectId + projectName + role + streamId + streamName + title + token + user { + id, + name, + bio, + company, + verified, + role, + } + } + } + } + """; + + var request = new GraphQLRequest { Query = QUERY }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + if (response.activeUser is null) + { + throw new SpeckleGraphQLException("GraphQL response indicated that the ActiveUser could not be found"); + } + + return response.activeUser.projectInvites; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/CommentResource.cs b/Core/Core/Api/GraphQL/Resources/CommentResource.cs new file mode 100644 index 0000000000..bd23ae8e5c --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/CommentResource.cs @@ -0,0 +1,279 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class CommentResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal CommentResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// Max number of comments to fetch + /// Optional cursor for pagination + /// Optional filter + /// Max number of comment replies to fetch + /// Optional cursor for pagination + /// + /// + /// + public async Task> GetProjectComments( + string projectId, + int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? cursor = null, + ProjectCommentsFilter? filter = null, + int repliesLimit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? repliesCursor = null, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query CommentThreads($projectId: String!, $cursor: String, $limit: Int!, $filter: ProjectCommentsFilter, $repliesLimit: Int, $repliesCursor: String) { + project(id: $projectId) { + commentThreads(cursor: $cursor, limit: $limit, filter: $filter) { + cursor + totalArchivedCount + totalCount + items { + archived + authorId + createdAt + hasParent + id + rawText + replies(limit: $repliesLimit, cursor: $repliesCursor) { + cursor + items { + archived + authorId + createdAt + hasParent + id + rawText + updatedAt + viewedAt + } + totalCount + } + resources { + resourceId + resourceType + } + screenshot + updatedAt + viewedAt + viewerResources { + modelId + objectId + versionId + } + data + } + } + } + } + """; + + GraphQLRequest request = + new() + { + Query = QUERY, + Variables = new + { + projectId, + cursor, + limit, + filter, + repliesLimit, + repliesCursor, + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.project.commentThreads; + } + + /// + /// This function only exists here to be able to integration tests the queries. + /// The process of creating a comment is more complex and javascript specific than we can expose to our SDKs at this time. + /// + /// + /// + /// + /// + internal async Task Create(CreateCommentInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation Mutation($input: CreateCommentInput!) { + data:commentMutations { + create(input: $input) { + archived + authorId + createdAt + hasParent + id + rawText + resources { + resourceId + resourceType + } + screenshot + updatedAt + viewedAt + viewerResources { + modelId + objectId + versionId + } + data + } + } + } + """; + GraphQLRequest request = new(QUERY, variables: new { input }); + var res = await _client + .ExecuteGraphQLRequest>(request, cancellationToken) + .ConfigureAwait(false); + return res.data.create; + } + + /// + /// + /// + /// + /// + internal async Task Edit(EditCommentInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation Mutation($input: EditCommentInput!) { + data:commentMutations { + edit(input: $input) { + archived + authorId + createdAt + hasParent + id + rawText + resources { + resourceId + resourceType + } + screenshot + updatedAt + viewedAt + viewerResources { + modelId + objectId + versionId + } + data + } + } + } + """; + GraphQLRequest request = new(QUERY, variables: new { input }); + var res = await _client + .ExecuteGraphQLRequest>(request, cancellationToken) + .ConfigureAwait(false); + return res.data.edit; + } + + /// + /// + /// + /// + /// + public async Task Archive(string commentId, bool archive = true, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation Mutation($commentId: String!, $archive: Boolean!) { + data:commentMutations { + archive(commentId: $commentId, archived: $archive) + } + } + """; + GraphQLRequest request = new(QUERY, variables: new { commentId, archive }); + var res = await _client + .ExecuteGraphQLRequest>(request, cancellationToken) + .ConfigureAwait(false); + return res.data.archive; + } + + /// + /// + /// + /// + public async Task MarkViewed(string commentId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation Mutation($commentId: String!) { + data:commentMutations { + markViewed(commentId: $commentId) + } + } + """; + GraphQLRequest request = new(QUERY, variables: new { commentId }); + var res = await _client + .ExecuteGraphQLRequest>(request, cancellationToken) + .ConfigureAwait(false); + return res.data.markViewed; + } + + /// + /// + /// + /// + /// + internal async Task Reply(CreateCommentReplyInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation Mutation($input: CreateCommentReplyInput!) { + data:commentMutations { + reply(input: $input) { + archived + authorId + createdAt + hasParent + id + rawText + resources { + resourceId + resourceType + } + screenshot + updatedAt + viewedAt + viewerResources { + modelId + objectId + versionId + } + data + } + } + } + """; + GraphQLRequest request = new(QUERY, variables: new { input }); + var res = await _client + .ExecuteGraphQLRequest>(request, cancellationToken) + .ConfigureAwait(false); + return res.data.reply; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/ModelResource.cs b/Core/Core/Api/GraphQL/Resources/ModelResource.cs new file mode 100644 index 0000000000..95b3514a2b --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/ModelResource.cs @@ -0,0 +1,309 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class ModelResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal ModelResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// + /// + /// + /// + /// + public async Task Get(string modelId, string projectId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query ModelGet($modelId: String!, $projectId: String!) { + project(id: $projectId) { + model(id: $modelId) { + id + name + previewUrl + updatedAt + description + displayName + createdAt + author { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + } + } + """; + var request = new GraphQLRequest { Query = QUERY, Variables = new { modelId, projectId } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.project.model; + } + + /// + /// + /// Max number of versions to fetch + /// Optional cursor for pagination + /// Optional versions filter + /// + /// + /// + /// + public async Task GetWithVersions( + string modelId, + string projectId, + int versionsLimit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? versionsCursor = null, + ModelVersionsFilter? versionsFilter = null, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query ModelGetWithVersions($modelId: String!, $projectId: String!, $versionsLimit: Int!, $versionsCursor: String, $versionsFilter: ModelVersionsFilter) { + project(id: $projectId) { + model(id: $modelId) { + id + name + previewUrl + updatedAt + versions(limit: $versionsLimit, cursor: $versionsCursor, filter: $versionsFilter) { + items { + id + referencedObject + message + sourceApplication + createdAt + previewUrl + authorUser { + totalOwnedStreamsFavorites + id + name + bio + company + verified + role + } + } + totalCount + cursor + } + description + displayName + createdAt + author { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + } + } + """; + + var request = new GraphQLRequest + { + Query = QUERY, + Variables = new + { + projectId, + modelId, + versionsLimit, + versionsCursor, + versionsFilter, + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.project.model; + } + + /// + /// Max number of models to fetch + /// Optional cursor for pagination + /// Optional models filter + /// + /// + /// + public async Task> GetModels( + string projectId, + int modelsLimit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? modelsCursor = null, + ProjectModelsFilter? modelsFilter = null, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) { + project(id: $projectId) { + models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) { + items { + id + name + previewUrl + updatedAt + displayName + description + createdAt + } + totalCount + cursor + } + } + } + """; + GraphQLRequest request = + new() + { + Query = QUERY, + Variables = new + { + projectId, + modelsLimit, + modelsCursor, + modelsFilter + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project.models; + } + + /// + /// + /// + /// + public async Task Create(CreateModelInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ModelCreate($input: CreateModelInput!) { + modelMutations { + create(input: $input) { + id + displayName + name + description + createdAt + updatedAt + previewUrl + author { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + } + } + """; + + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var res = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return res.modelMutations.create; + } + + /// + /// + /// + /// + public async Task Delete(DeleteModelInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ModelDelete($input: DeleteModelInput!) { + modelMutations { + delete(input: $input) + } + } + """; + + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var res = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return res.modelMutations.delete; + } + + /// + /// + /// + /// + public async Task Update(UpdateModelInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ModelUpdate($input: UpdateModelInput!) { + modelMutations { + update(input: $input) { + id + name + displayName + description + createdAt + updatedAt + previewUrl + author { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + } + } + """; + + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var res = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return res.modelMutations.update; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/OtherUserResource.cs b/Core/Core/Api/GraphQL/Resources/OtherUserResource.cs new file mode 100644 index 0000000000..5f51ced885 --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/OtherUserResource.cs @@ -0,0 +1,108 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class OtherUserResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal OtherUserResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// + /// + /// + /// + /// the requested user, or null if the user does not exist + /// + public async Task Get(string id, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query LimitedUser($id: String!) { + otherUser(id: $id){ + id, + name, + bio, + company, + avatar, + verified, + role, + } + } + """; + + var request = new GraphQLRequest { Query = QUERY, Variables = new { id } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.otherUser; + } + + /// + /// Searches for a user on the server. + /// + /// String to search for. Must be at least 3 characters + /// Max number of users to fetch + /// Optional cursor for pagination + /// + /// + /// + /// + /// + public async Task> UserSearch( + string query, + int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? cursor = null, + bool archived = false, + bool emailOnly = false, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) { + userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) { + cursor, + items { + id + name + bio + company + avatar + verified + role + } + } + } + """; + + var request = new GraphQLRequest + { + Query = QUERY, + Variables = new + { + query, + limit, + cursor, + archived, + emailOnly + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.userSearch; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/ProjectInviteResource.cs b/Core/Core/Api/GraphQL/Resources/ProjectInviteResource.cs new file mode 100644 index 0000000000..65f1cb1e30 --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/ProjectInviteResource.cs @@ -0,0 +1,260 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class ProjectInviteResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal ProjectInviteResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// + /// + /// + /// + public async Task Create( + string projectId, + ProjectInviteCreateInput input, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + mutation ProjectInviteCreate($projectId: ID!, $input: ProjectInviteCreateInput!) { + projectMutations { + invites { + create(projectId: $projectId, input: $input) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + team { + role + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + invitedTeam { + id + inviteId + projectId + projectName + streamName + title + role + streamId + token + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + invitedBy { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.projectMutations.invites.create; + } + + /// + /// + /// + /// + public async Task Use(ProjectInviteUseInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectInviteUse($input: ProjectInviteUseInput!) { + projectMutations { + invites { + use(input: $input) + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.projectMutations.invites.use; + } + + /// + /// + /// + /// The invite, or null if no invite exists + /// + public async Task Get( + string projectId, + string? token, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query ProjectInvite($projectId: String!, $token: String) { + projectInvite(projectId: $projectId, token: $token) { + id + inviteId + invitedBy { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + projectId + projectName + role + streamId + streamName + title + token + user { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, token } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.projectInvite; + } + + /// + /// + /// + /// + /// + public async Task Cancel(string projectId, string inviteId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectInviteCancel($projectId: ID!, $inviteId: String!) { + projectMutations { + invites { + cancel(projectId: $projectId, inviteId: $inviteId) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + team { + role + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + invitedTeam { + id + inviteId + projectId + projectName + streamName + title + role + streamId + token + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + invitedBy { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, inviteId } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.projectMutations.invites.cancel; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/ProjectResource.cs b/Core/Core/Api/GraphQL/Resources/ProjectResource.cs new file mode 100644 index 0000000000..f21fb723ae --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/ProjectResource.cs @@ -0,0 +1,349 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class ProjectResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal ProjectResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// + /// + /// + /// + /// + public async Task Get(string projectId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query Project($projectId: String!) { + project(id: $projectId) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project; + } + + /// + /// Max number of models to fetch + /// Optional cursor for pagination + /// Optional models filter + /// + /// + /// + /// + /// + public async Task GetWithModels( + string projectId, + int modelsLimit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? modelsCursor = null, + ProjectModelsFilter? modelsFilter = null, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) { + project(id: $projectId) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) { + items { + id + name + previewUrl + updatedAt + displayName + description + createdAt + } + cursor + totalCount + } + } + } + """; + GraphQLRequest request = + new() + { + Query = QUERY, + Variables = new + { + projectId, + modelsLimit, + modelsCursor, + modelsFilter + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project; + } + + /// + /// + /// + /// + /// + /// + public async Task GetWithTeam(string projectId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + query ProjectGetWithTeam($projectId: String!) { + project(id: $projectId) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + team { + role + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + invitedTeam { + id + inviteId + projectId + projectName + streamId + streamName + title + role + token + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + invitedBy { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project; + } + + /// + /// + /// + /// + public async Task Create(ProjectCreateInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectCreate($input: ProjectCreateInput) { + projectMutations { + create(input: $input) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.projectMutations.create; + } + + /// + /// + /// + /// + public async Task Update(ProjectUpdateInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectUpdate($input: ProjectUpdateInput!) { + projectMutations{ + update(update: $input) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.projectMutations.update; + } + + /// The id of the Project to delete + /// + /// + /// + public async Task Delete(string projectId, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectDelete($projectId: String!) { + projectMutations { + delete(id: $projectId) + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.projectMutations.delete; + } + + /// + /// + /// + public async Task UpdateRole(ProjectUpdateRoleInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation ProjectUpdateRole($input: ProjectUpdateRoleInput!) { + projectMutations { + updateRole(input: $input) { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + team { + role + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + invitedTeam { + id + inviteId + projectId + projectName + streamId + streamName + title + role + token + user { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + invitedBy { + totalOwnedStreamsFavorites + id + name + bio + company + avatar + verified + role + } + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.projectMutations.updateRole; + } +} diff --git a/Core/Core/Api/GraphQL/Resources/SubscriptionResource.cs b/Core/Core/Api/GraphQL/Resources/SubscriptionResource.cs new file mode 100644 index 0000000000..d476ae71e0 --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/SubscriptionResource.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class Subscription : IDisposable + where TEventArgs : EventArgs +{ + internal Subscription(ISpeckleGraphQLClient client, GraphQLRequest request) + { + _subscription = client.SubscribeTo>(request, (o, t) => Listeners?.Invoke(o, t.data)); + } + + public event EventHandler? Listeners; + + private readonly IDisposable _subscription; + + public void Dispose() + { + _subscription.Dispose(); + } +} + +public sealed class SubscriptionResource : IDisposable +{ + private readonly ISpeckleGraphQLClient _client; + private readonly List _subscriptions; + + internal SubscriptionResource(ISpeckleGraphQLClient client) + { + _client = client; + _subscriptions = new(); + } + + /// Track newly added or deleted projects owned by the active user + /// + /// You should add event listeners to the returned object.
+ /// You can add multiple listeners to a , and this should be preferred over creating many subscriptions.
+ /// You should ensure proper disposal of the when you're done (see )
+ /// Disposing of the or will also dispose any s it created. + ///
+ /// + public Subscription CreateUserProjectsUpdatedSubscription() + { + //language=graphql + const string QUERY = """ + subscription UserProjectsUpdated { + data:userProjectsUpdated { + id + project { + id + } + type + } + } + """; + GraphQLRequest request = new() { Query = QUERY }; + + Subscription subscription = new(_client, request); + _subscriptions.Add(subscription); + return subscription; + } + + /// Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources + /// + /// + public Subscription CreateProjectCommentsUpdatedSubscription( + ViewerUpdateTrackingTarget target + ) + { + //language=graphql + const string QUERY = """ + subscription Subscription($target: ViewerUpdateTrackingTarget!) { + data:projectCommentsUpdated(target: $target) { + comment { + id + } + id + type + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { target } }; + + Subscription subscription = new(_client, request); + _subscriptions.Add(subscription); + return subscription; + } + + /// Subscribe to changes to a project's models. Optionally specify to track + /// + /// + public Subscription CreateProjectModelsUpdatedSubscription( + string id, + IReadOnlyList? modelIds = null + ) + { + //language=graphql + const string QUERY = """ + subscription ProjectModelsUpdated($id: String!, $modelIds: [String!]) { + data:projectModelsUpdated(id: $id, modelIds: $modelIds) { + id + model { + id + name + previewUrl + updatedAt + description + displayName + createdAt + author { + avatar + bio + company + id + name + role + totalOwnedStreamsFavorites + verified + } + } + type + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { id, modelIds } }; + + Subscription subscription = new(_client, request); + _subscriptions.Add(subscription); + return subscription; + } + + /// Track updates to a specific project + /// + /// + public Subscription CreateProjectUpdatedSubscription(string id) + { + //language=graphql + const string QUERY = """ + subscription ProjectUpdated($id: String!) { + data:projectUpdated(id: $id) { + id + project { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + } + type + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { id } }; + + Subscription subscription = new(_client, request); + _subscriptions.Add(subscription); + return subscription; + } + + /// Subscribe to changes to a project's versions. + /// + /// + public Subscription CreateProjectVersionsUpdatedSubscription(string id) + { + //language=graphql + const string QUERY = """ + subscription ProjectVersionsUpdated($id: String!) { + data:projectVersionsUpdated(id: $id) { + id + modelId + type + version { + id + referencedObject + message + sourceApplication + createdAt + previewUrl + authorUser { + totalOwnedStreamsFavorites + id + name + bio + company + verified + role + avatar + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { id } }; + + Subscription subscription = new(_client, request); + _subscriptions.Add(subscription); + return subscription; + } + + public void Dispose() + { + foreach (var subscription in _subscriptions) + { + subscription.Dispose(); + } + } +} diff --git a/Core/Core/Api/GraphQL/Resources/VersionResource.cs b/Core/Core/Api/GraphQL/Resources/VersionResource.cs new file mode 100644 index 0000000000..e21ac09e0c --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/VersionResource.cs @@ -0,0 +1,252 @@ +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Models.Responses; +using Version = Speckle.Core.Api.GraphQL.Models.Version; + +namespace Speckle.Core.Api.GraphQL.Resources; + +public sealed class VersionResource +{ + private readonly ISpeckleGraphQLClient _client; + + internal VersionResource(ISpeckleGraphQLClient client) + { + _client = client; + } + + /// + /// + /// + /// + /// + /// + public async Task Get( + string versionId, + string modelId, + string projectId, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query VersionGet($projectId: String!, $modelId: String!, $versionId: String!) { + project(id: $projectId) { + model(id: $modelId) { + version(id: $versionId) { + id + referencedObject + message + sourceApplication + createdAt + previewUrl + authorUser { + totalOwnedStreamsFavorites + id + name + bio + company + verified + role + avatar + } + } + } + } + } + """; + GraphQLRequest request = + new() + { + Query = QUERY, + Variables = new + { + projectId, + modelId, + versionId + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project.model.version; + } + + /// + /// + /// Max number of versions to fetch + /// Optional cursor for pagination + /// Optional filter + /// + /// + public async Task> GetVersions( + string modelId, + string projectId, + int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST, + string? cursor = null, + ModelVersionsFilter? filter = null, + CancellationToken cancellationToken = default + ) + { + //language=graphql + const string QUERY = """ + query VersionGetVersions($projectId: String!, $modelId: String!, $limit: Int!, $cursor: String, $filter: ModelVersionsFilter) { + project(id: $projectId) { + model(id: $modelId) { + versions(limit: $limit, cursor: $cursor, filter: $filter) { + items { + id + referencedObject + message + sourceApplication + createdAt + previewUrl + authorUser { + totalOwnedStreamsFavorites + id + name + bio + company + verified + role + avatar + } + } + cursor + totalCount + } + } + } + } + """; + + GraphQLRequest request = + new() + { + Query = QUERY, + Variables = new + { + projectId, + modelId, + limit, + cursor, + filter, + } + }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.project.model.versions; + } + + /// + /// + /// + public async Task Create(CommitCreateInput input, CancellationToken cancellationToken = default) + { + //TODO: Implement on server + return await ((Client)_client).CommitCreate(input, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + public async Task Update(UpdateVersionInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation VersionUpdate($input: UpdateVersionInput!) { + versionMutations { + update(input: $input) { + id + referencedObject + message + sourceApplication + createdAt + previewUrl + authorUser { + totalOwnedStreamsFavorites + id + name + bio + company + verified + role + avatar + } + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input, } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.versionMutations.update; + } + + //TODO: Would we rather return the full model here? with or with out versions? + /// + /// + /// + /// + public async Task MoveToModel(MoveVersionsInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation VersionMoveToModel($input: MoveVersionsInput!) { + versionMutations { + moveToModel(input: $input) { + id + } + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input, } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + return response.versionMutations.moveToModel.id; + } + + /// + /// + /// + public async Task Delete(DeleteVersionsInput input, CancellationToken cancellationToken = default) + { + //language=graphql + const string QUERY = """ + mutation VersionDelete($input: DeleteVersionsInput!) { + versionMutations { + delete(input: $input) + } + } + """; + GraphQLRequest request = new() { Query = QUERY, Variables = new { input } }; + + var response = await _client + .ExecuteGraphQLRequest(request, cancellationToken) + .ConfigureAwait(false); + + return response.versionMutations.delete; + } + + /// + /// + /// + /// + public async Task Received( + CommitReceivedInput commitReceivedInput, + CancellationToken cancellationToken = default + ) + { + //TODO: Implement on server + return await ((Client)_client).CommitReceived(commitReceivedInput, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Core/Core/Api/GraphQL/Resources/graphql.config.yml b/Core/Core/Api/GraphQL/Resources/graphql.config.yml new file mode 100644 index 0000000000..64c50ab285 --- /dev/null +++ b/Core/Core/Api/GraphQL/Resources/graphql.config.yml @@ -0,0 +1,2 @@ +schema: https://app.speckle.systems/graphql +documents: '**/*.graphql' diff --git a/Core/Core/Api/GraphQL/StreamRoles.cs b/Core/Core/Api/GraphQL/StreamRoles.cs new file mode 100644 index 0000000000..963fd2dd11 --- /dev/null +++ b/Core/Core/Api/GraphQL/StreamRoles.cs @@ -0,0 +1,12 @@ +namespace Speckle.Core.Api.GraphQL; + +/// +/// These are the default roles used by the server +/// +public static class StreamRoles +{ + public const string STREAM_OWNER = "stream:owner"; + public const string STREAM_CONTRIBUTOR = "stream:contributor"; + public const string STREAM_REVIEWER = "stream:reviewer"; + public const string? REVOKE = null; +} diff --git a/Core/Core/Api/Helpers.cs b/Core/Core/Api/Helpers.cs index dc3e2ef1c4..1450074229 100644 --- a/Core/Core/Api/Helpers.cs +++ b/Core/Core/Api/Helpers.cs @@ -10,6 +10,8 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Threading.Tasks; +using Speckle.Core.Api.GraphQL; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; using Speckle.Core.Helpers; using Speckle.Core.Kits; diff --git a/Core/Core/Api/ServerLimits.cs b/Core/Core/Api/ServerLimits.cs index 7f78def96a..262d1a63fc 100644 --- a/Core/Core/Api/ServerLimits.cs +++ b/Core/Core/Api/ServerLimits.cs @@ -11,4 +11,7 @@ public static class ServerLimits { public const int BRANCH_GET_LIMIT = 500; public const int OLD_BRANCH_GET_LIMIT = 100; + + /// the default `limit` argument value for paginated requests + public const int DEFAULT_PAGINATION_REQUEST = 25; } diff --git a/Core/Core/Credentials/Account.cs b/Core/Core/Credentials/Account.cs index 2774a7a40d..4f4bf19173 100644 --- a/Core/Core/Credentials/Account.cs +++ b/Core/Core/Credentials/Account.cs @@ -2,7 +2,7 @@ using System; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Helpers; namespace Speckle.Core.Credentials; diff --git a/Core/Core/Credentials/AccountManager.cs b/Core/Core/Credentials/AccountManager.cs index 0f9138825f..bfd619f5c8 100644 --- a/Core/Core/Credentials/AccountManager.cs +++ b/Core/Core/Credentials/AccountManager.cs @@ -14,7 +14,9 @@ using GraphQL; using GraphQL.Client.Http; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Api.GraphQL; +using Speckle.Core.Api.GraphQL.Models.Responses; using Speckle.Core.Api.GraphQL.Serializer; using Speckle.Core.Helpers; using Speckle.Core.Logging; @@ -114,22 +116,29 @@ public static async Task GetUserInfo( new NewtonsoftJsonSerializer(), httpClient ); - //language=graphql - var request = new GraphQLRequest { Query = " query { activeUser { name email id company } }" }; - - var response = await gqlClient.SendQueryAsync(request, cancellationToken).ConfigureAwait(false); + const string QUERY = """ + query { + data:activeUser { + name + email + id + company + } + } + """; + var request = new GraphQLRequest { Query = QUERY }; + + var response = await gqlClient + .SendQueryAsync>(request, cancellationToken) + .ConfigureAwait(false); if (response.Errors != null) { - throw new SpeckleGraphQLException( - $"GraphQL request {nameof(GetUserInfo)} failed", - request, - response - ); + throw new SpeckleGraphQLException($"GraphQL request {nameof(GetUserInfo)} failed", request, response); } - return response.Data.activeUser; + return response.Data.data; } /// diff --git a/Core/Core/Credentials/Responses.cs b/Core/Core/Credentials/Responses.cs index 04eb87cfc5..066dfde6d8 100644 --- a/Core/Core/Credentials/Responses.cs +++ b/Core/Core/Credentials/Responses.cs @@ -1,56 +1,42 @@ -#nullable disable using System; using System.Runtime.InteropServices; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; namespace Speckle.Core.Credentials; -[Obsolete("Use activeUser query and ActiveUserServerInfoResponse instead", true)] -public class UserServerInfoResponse +internal sealed class ActiveUserServerInfoResponse { - public UserInfo user { get; set; } - public ServerInfo serverInfo { get; set; } + public UserInfo activeUser { get; init; } + public ServerInfo serverInfo { get; init; } } -public class ActiveUserServerInfoResponse +internal sealed class TokenExchangeResponse { - public UserInfo activeUser { get; set; } - public ServerInfo serverInfo { get; set; } -} - -[Obsolete("Use activeUser query and ActiveUserResponse instead", true)] -public class UserInfoResponse -{ - public UserInfo user { get; set; } -} - -public class ActiveUserResponse -{ - public UserInfo activeUser { get; set; } + public string token { get; init; } + public string refreshToken { get; init; } } [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] -public class UserInfo +public sealed class UserInfo { - public string id { get; set; } - public string name { get; set; } - public string email { get; set; } - public string company { get; set; } - public string avatar { get; set; } + public string id { get; init; } + public string name { get; init; } + public string email { get; init; } + public string? company { get; init; } + public string? avatar { get; init; } - public Streams streams { get; set; } - public Commits commits { get; set; } -} + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public Streams streams { get; init; } -public class TokenExchangeResponse -{ - public string token { get; set; } - public string refreshToken { get; set; } + [Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] + public Commits commits { get; init; } } [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Streams { public int totalCount { get; set; } @@ -58,6 +44,7 @@ public class Streams [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] +[Obsolete(DeprecationMessages.FE2_DEPRECATION_MESSAGE)] public class Commits { public int totalCount { get; set; } diff --git a/Core/Core/Credentials/StreamWrapper.cs b/Core/Core/Credentials/StreamWrapper.cs index 7c9d49caa2..5c578c46f3 100644 --- a/Core/Core/Credentials/StreamWrapper.cs +++ b/Core/Core/Credentials/StreamWrapper.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Web; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Helpers; using Speckle.Core.Logging; diff --git a/Core/Core/Logging/SpeckleException.cs b/Core/Core/Logging/SpeckleException.cs index 397b8b16ac..332d59501f 100644 --- a/Core/Core/Logging/SpeckleException.cs +++ b/Core/Core/Logging/SpeckleException.cs @@ -3,6 +3,7 @@ using System.Linq; using GraphQL; using Sentry; +using Speckle.Core.Api; namespace Speckle.Core.Logging; @@ -21,7 +22,7 @@ public SpeckleException(string? message, Exception? inner = null) public SpeckleException(string? message, Exception? inner, bool log = true, SentryLevel level = SentryLevel.Info) : base(message, inner) { } - [Obsolete("Use any other constructor")] + [Obsolete($"Use {nameof(SpeckleGraphQLException)} instead", true)] public SpeckleException(string? message, GraphQLError[] errors, bool log = true, SentryLevel level = SentryLevel.Info) : base(message) { @@ -32,7 +33,7 @@ public SpeckleException(string? message, GraphQLError[] errors, bool log = true, public SpeckleException(string message, bool log, SentryLevel level = SentryLevel.Info) : base(message) { } - [Obsolete("Use any other constructor", true)] + [Obsolete($"Use {nameof(SpeckleGraphQLException)} instead", true)] public List> GraphQLErrors { get; set; } #endregion } diff --git a/Core/Core/SharpResources.cs b/Core/Core/SharpResources.cs new file mode 100644 index 0000000000..3a7998cfa2 --- /dev/null +++ b/Core/Core/SharpResources.cs @@ -0,0 +1,6 @@ +#if !NET5_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit { } + +#endif diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/LegacyAPITests.cs similarity index 98% rename from Core/Tests/Speckle.Core.Tests.Integration/Api.cs rename to Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/LegacyAPITests.cs index bc3b574fcd..46b87d799d 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Api.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/LegacyAPITests.cs @@ -1,12 +1,13 @@ using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL; using Speckle.Core.Credentials; using Speckle.Core.Models; using Speckle.Core.Tests.Unit.Kits; using Speckle.Core.Transports; -namespace Speckle.Core.Tests.Integration; +namespace Speckle.Core.Tests.Integration.Api.GraphQL.Legacy; -public class Api : IDisposable +public class LegacyAPITests : IDisposable { private string _branchId = ""; private string _branchName = ""; @@ -177,7 +178,7 @@ public async Task StreamUpdatePermission() var res = await _myClient.StreamUpdatePermission( new StreamPermissionInput { - role = "stream:reviewer", + role = StreamRoles.STREAM_REVIEWER, streamId = _streamId, userId = _secondUserAccount.userInfo.id } diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Branches.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Branches.cs similarity index 97% rename from Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Branches.cs rename to Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Branches.cs index dce806731c..2833a279c1 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Branches.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Branches.cs @@ -2,7 +2,7 @@ using Speckle.Core.Api.SubscriptionModels; using Speckle.Core.Credentials; -namespace Speckle.Core.Tests.Integration.Subscriptions; +namespace Speckle.Core.Tests.Integration.Api.GraphQL.Legacy.Subscriptions; public class Branches : IDisposable { diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Commits.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Commits.cs similarity index 98% rename from Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Commits.cs rename to Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Commits.cs index ed10de7b4c..7e25fad641 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Commits.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Commits.cs @@ -5,7 +5,7 @@ using Speckle.Core.Tests.Unit.Kits; using Speckle.Core.Transports; -namespace Speckle.Core.Tests.Integration.Subscriptions; +namespace Speckle.Core.Tests.Integration.Api.GraphQL.Legacy.Subscriptions; public class Commits : IDisposable { diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Streams.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Streams.cs similarity index 97% rename from Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Streams.cs rename to Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Streams.cs index 5acd4d71c9..457c2d6b41 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Subscriptions/Streams.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Legacy/Subscriptions/Streams.cs @@ -2,7 +2,7 @@ using Speckle.Core.Api.SubscriptionModels; using Speckle.Core.Credentials; -namespace Speckle.Core.Tests.Integration.Subscriptions; +namespace Speckle.Core.Tests.Integration.Api.GraphQL.Legacy.Subscriptions; public class Streams : IDisposable { diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ActiveUserResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ActiveUserResourceTests.cs new file mode 100644 index 0000000000..1b2e810fa2 --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ActiveUserResourceTests.cs @@ -0,0 +1,51 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ActiveUserResource))] +public class ActiveUserResourceTests +{ + private Client _testUser; + private ActiveUserResource Sut => _testUser.ActiveUser; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + } + + [Test] + public async Task ActiveUserGet() + { + var res = await Sut.Get(); + Assert.That(res.id, Is.EqualTo(_testUser.Account.userInfo.id)); + } + + [Test] + public async Task ActiveUserGet_NonAuthed() + { + var result = await Fixtures.Unauthed.ActiveUser.Get(); + Assert.That(result, Is.EqualTo(null)); + } + + [Test] + public async Task ActiveUserGetProjects() + { + var p1 = await _testUser.Project.Create(new("Project 1", null, null)); + var p2 = await _testUser.Project.Create(new("Project 2", null, null)); + + var res = await Sut.GetProjects(); + + Assert.That(res.items, Has.Exactly(1).Items.With.Property(nameof(Project.id)).EqualTo(p1.id)); + Assert.That(res.items, Has.Exactly(1).Items.With.Property(nameof(Project.id)).EqualTo(p2.id)); + Assert.That(res.items, Has.Count.EqualTo(2)); + } + + [Test] + public void ActiveUserGetProjects_NoAuth() + { + Assert.ThrowsAsync(async () => await Fixtures.Unauthed.ActiveUser.GetProjects()); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/CommentResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/CommentResourceTests.cs new file mode 100644 index 0000000000..cdf25948ea --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/CommentResourceTests.cs @@ -0,0 +1,97 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(CommentResource))] +public class CommentResourceTests +{ + private Client _testUser; + private CommentResource Sut => _testUser.Comment; + private Project _project; + private Model _model; + private string _versionId; + private Comment _comment; + + [SetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _project = await _testUser.Project.Create(new("Test project", "", null)); + _model = await _testUser.Model.Create(new("Test Model 1", "", _project.id)); + _versionId = await Fixtures.CreateVersion(_testUser, _project.id, _model.name); + _comment = await CreateComment(); + } + + [Test] + public async Task GetProjectComments() + { + var comments = await Sut.GetProjectComments(_project.id); + Assert.That(comments.items.Count, Is.EqualTo(1)); + Assert.That(comments.totalCount, Is.EqualTo(1)); + + Comment comment = comments.items[0]; + Assert.That(comment, Is.Not.Null); + Assert.That(comment, Has.Property(nameof(Comment.authorId)).EqualTo(_testUser.Account.userInfo.id)); + + Assert.That(comment, Has.Property(nameof(Comment.id)).EqualTo(_comment.id)); + Assert.That(comment, Has.Property(nameof(Comment.authorId)).EqualTo(_comment.authorId)); + Assert.That(comment, Has.Property(nameof(Comment.archived)).EqualTo(_comment.archived)); + Assert.That(comment, Has.Property(nameof(Comment.archived)).EqualTo(false)); + Assert.That(comment, Has.Property(nameof(Comment.createdAt)).EqualTo(_comment.createdAt)); + } + + [Test] + public async Task MarkViewed() + { + var viewed = await Sut.MarkViewed(_comment.id); + Assert.That(viewed, Is.True); + viewed = await Sut.MarkViewed(_comment.id); + Assert.That(viewed, Is.True); + } + + [Test] + public async Task Archive() + { + var archived = await Sut.Archive(_comment.id); + Assert.That(archived, Is.True); + + archived = await Sut.Archive(_comment.id); + Assert.That(archived, Is.True); + } + + [Test] + public async Task Edit() + { + var blobs = await Fixtures.SendBlobData(_testUser.Account, _project.id); + var blobIds = blobs.Select(b => b.id).ToList(); + EditCommentInput input = new(new(blobIds, null), _comment.id); + + var editedComment = await Sut.Edit(input); + + Assert.That(editedComment, Is.Not.Null); + Assert.That(editedComment, Has.Property(nameof(Comment.id)).EqualTo(_comment.id)); + Assert.That(editedComment, Has.Property(nameof(Comment.authorId)).EqualTo(_comment.authorId)); + Assert.That(editedComment, Has.Property(nameof(Comment.createdAt)).EqualTo(_comment.createdAt)); + Assert.That(editedComment, Has.Property(nameof(Comment.updatedAt)).GreaterThanOrEqualTo(_comment.updatedAt)); + } + + [Test] + public async Task Reply() + { + var blobs = await Fixtures.SendBlobData(_testUser.Account, _project.id); + var blobIds = blobs.Select(b => b.id).ToList(); + CreateCommentReplyInput input = new(new(blobIds, null), _comment.id); + + var editedComment = await Sut.Reply(input); + + Assert.That(editedComment, Is.Not.Null); + } + + private async Task CreateComment() + { + return await Fixtures.CreateComment(_testUser, _project.id, _model.id, _versionId); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceExceptionalTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceExceptionalTests.cs new file mode 100644 index 0000000000..8fe43ba10c --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceExceptionalTests.cs @@ -0,0 +1,88 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Enums; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ModelResource))] +public class ModelResourceExceptionalTests +{ + private Client _testUser; + private ModelResource Sut => _testUser.Model; + private Project _project; + private Model _model; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _project = await _testUser.Project.Create(new("Test project", "", ProjectVisibility.Private)); + _model = await _testUser.Model.Create(new("Test Model", "", _project.id)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void ModelCreate_Throws_InvalidInput(string name) + { + CreateModelInput input = new(name, null, _project.id); + Assert.CatchAsync(async () => await Sut.Create(input)); + } + + [Test] + public void ModelGet_Throws_NoAuth() + { + Assert.CatchAsync(async () => await Fixtures.Unauthed.Model.Get(_model.id, _project.id)); + } + + [Test] + public void ModelGet_Throws_NonExistentModel() + { + Assert.CatchAsync(async () => await Sut.Get("non existent model", _project.id)); + } + + [Test] + public void ModelGet_Throws_NonExistentProject() + { + Assert.ThrowsAsync( + async () => await Sut.Get(_model.id, "non existent project") + ); + } + + [Test] + public void ModelUpdate_Throws_NonExistentModel() + { + UpdateModelInput input = new("non-existent model", "MY new name", "MY new desc", _project.id); + + Assert.CatchAsync(async () => await Sut.Update(input)); + } + + [Test] + public void ModelUpdate_Throws_NonExistentProject() + { + UpdateModelInput input = new(_model.id, "MY new name", "MY new desc", "non-existent project"); + + Assert.ThrowsAsync(async () => await Sut.Update(input)); + } + + [Test] + public void ModelUpdate_Throws_NonAuthProject() + { + UpdateModelInput input = new(_model.id, "MY new name", "MY new desc", _project.id); + + Assert.CatchAsync(async () => await Fixtures.Unauthed.Model.Update(input)); + } + + [Test] + public async Task ModelDelete_Throws_NoAuth() + { + Model toDelete = await Sut.Create(new("Delete me", null, _project.id)); + DeleteModelInput input = new(toDelete.id, _project.id); + bool response = await Sut.Delete(input); + Assert.That(response, Is.True); + + Assert.CatchAsync(async () => _ = await Sut.Delete(input)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceTests.cs new file mode 100644 index 0000000000..e2995acf29 --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ModelResourceTests.cs @@ -0,0 +1,96 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ModelResource))] +public class ModelResourceTests +{ + private Client _testUser; + private ModelResource Sut => _testUser.Model; + private Project _project; + private Model _model; + + [SetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _project = await _testUser.Project.Create(new("Test project", "", null)); + _model = await _testUser.Model.Create(new("Test Model", "", _project.id)); + } + + [TestCase("My Model", "My model description")] + [TestCase("my/nested/model", null)] + public async Task ModelCreate(string name, string description) + { + CreateModelInput input = new(name, description, _project.id); + Model result = await Sut.Create(input); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Property(nameof(result.id)).Not.Null); + Assert.That(result, Has.Property(nameof(result.name)).EqualTo(input.name).IgnoreCase); + Assert.That(result, Has.Property(nameof(result.description)).EqualTo(input.description)); + } + + [Test] + public async Task ModelGet() + { + Model result = await Sut.Get(_model.id, _project.id); + + Assert.That(result.id, Is.EqualTo(_model.id)); + Assert.That(result.name, Is.EqualTo(_model.name)); + Assert.That(result.description, Is.EqualTo(_model.description)); + Assert.That(result.createdAt, Is.EqualTo(_model.createdAt)); + Assert.That(result.updatedAt, Is.EqualTo(_model.updatedAt)); + } + + [Test] + public async Task GetModels() + { + var result = await Sut.GetModels(_project.id); + + Assert.That(result.items, Has.Count.EqualTo(1)); + Assert.That(result.totalCount, Is.EqualTo(1)); + Assert.That(result.items[0], Has.Property(nameof(Model.id)).EqualTo(_model.id)); + } + + [Test] + public async Task Project_GetModels() + { + var result = await _testUser.Project.GetWithModels(_project.id); + + Assert.That(result, Has.Property(nameof(Project.id)).EqualTo(_project.id)); + Assert.That(result.models.items, Has.Count.EqualTo(1)); + Assert.That(result.models.totalCount, Is.EqualTo(1)); + Assert.That(result.models.items[0], Has.Property(nameof(Model.id)).EqualTo(_model.id)); + } + + [Test] + public async Task ModelUpdate() + { + const string NEW_NAME = "MY new name"; + const string NEW_DESCRIPTION = "MY new desc"; + + UpdateModelInput input = new(_model.id, NEW_NAME, NEW_DESCRIPTION, _project.id); + Model updatedModel = await Sut.Update(input); + + Assert.That(updatedModel.id, Is.EqualTo(_model.id)); + Assert.That(updatedModel.name, Is.EqualTo(NEW_NAME).IgnoreCase); + Assert.That(updatedModel.description, Is.EqualTo(NEW_DESCRIPTION)); + Assert.That(updatedModel.updatedAt, Is.GreaterThanOrEqualTo(_model.updatedAt)); + } + + [Test] + public async Task ModelDelete() + { + DeleteModelInput input = new(_model.id, _project.id); + + bool response = await Sut.Delete(input); + Assert.That(response, Is.True); + + Assert.CatchAsync(async () => _ = await Sut.Get(_model.id, _project.id)); + Assert.CatchAsync(async () => _ = await Sut.Delete(input)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/OtherUserResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/OtherUserResourceTests.cs new file mode 100644 index 0000000000..9959131e88 --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/OtherUserResourceTests.cs @@ -0,0 +1,49 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Resources; +using Speckle.Core.Credentials; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(OtherUserResource))] +public class OtherUserResourceTests +{ + private Client _testUser; + private Account _testData; + private OtherUserResource Sut => _testUser.OtherUser; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _testData = await Fixtures.SeedUser(); + } + + [Test] + public async Task OtherUserGet() + { + var res = await Sut.Get(_testData.userInfo.id); + Assert.That(res.name, Is.EqualTo(_testData.userInfo.name)); + } + + [Test] + public async Task OtherUserGet_NonExistentUser() + { + var result = await Sut.Get("AnIdThatDoesntExist"); + Assert.That(result, Is.Null); + } + + [Test] + public async Task UserSearch() + { + var res = await Sut.UserSearch(_testData.userInfo.email, 25); + Assert.That(res.items, Has.Count.EqualTo(1)); + Assert.That(res.items[0].id, Is.EqualTo(_testData.userInfo.id)); + } + + [Test] + public async Task UserSearch_NonExistentUser() + { + var res = await Sut.UserSearch("idontexist@example.com", 25); + Assert.That(res.items, Has.Count.EqualTo(0)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceExceptionalTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceExceptionalTests.cs new file mode 100644 index 0000000000..b2cee38c2a --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceExceptionalTests.cs @@ -0,0 +1,32 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ProjectInviteResource))] +public class ProjectInviteResourceExceptionalTests +{ + private Client _testUser; + private Project _project; + private ProjectInviteResource Sut => _testUser.ProjectInvite; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _project = await _testUser.Project.Create(new("test", null, null)); + } + + [TestCase(null, null, null, null)] + [TestCase(null, "something", "something", null)] + public void ProjectInviteCreate_InvalidInput(string email, string role, string serverRole, string userId) + { + Assert.CatchAsync(async () => + { + var input = new ProjectInviteCreateInput(email, role, serverRole, userId); + await Sut.Create(_project.id, input); + }); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceTests.cs new file mode 100644 index 0000000000..f6268db573 --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectInviteResourceTests.cs @@ -0,0 +1,107 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ProjectInviteResource))] +public class ProjectInviteResourceTests +{ + private Client _inviter, + _invitee; + private Project _project; + private PendingStreamCollaborator _createdInvite; + + [SetUp] + public async Task Setup() + { + _inviter = await Fixtures.SeedUserWithClient(); + _invitee = await Fixtures.SeedUserWithClient(); + _project = await _inviter.Project.Create(new("test", null, null)); + _createdInvite = await SeedInvite(); + } + + private async Task SeedInvite() + { + ProjectInviteCreateInput input = new(_invitee.Account.userInfo.email, null, null, null); + var res = await _inviter.ProjectInvite.Create(_project.id, input); + var invites = await _invitee.ActiveUser.ProjectInvites(); + return invites.First(i => i.projectId == res.id); + } + + [Test] + public async Task ProjectInviteCreate_ByEmail() + { + ProjectInviteCreateInput input = new(_invitee.Account.userInfo.email, null, null, null); + var res = await _inviter.ProjectInvite.Create(_project.id, input); + + var invites = await _invitee.ActiveUser.ProjectInvites(); + var invite = invites.First(i => i.projectId == res.id); + + Assert.That(res, Has.Property(nameof(_project.id)).EqualTo(_project.id)); + Assert.That(res.invitedTeam, Has.Count.EqualTo(1)); + Assert.That(invite.user.id, Is.EqualTo(_invitee.Account.userInfo.id)); + Assert.That(invite.token, Is.Not.Null); + } + + [Test] + public async Task ProjectInviteCreate_ByUserId() + { + ProjectInviteCreateInput input = new(null, null, null, _invitee.Account.userInfo.id); + var res = await _inviter.ProjectInvite.Create(_project.id, input); + + Assert.That(res, Has.Property(nameof(_project.id)).EqualTo(_project.id)); + Assert.That(res.invitedTeam, Has.Count.EqualTo(1)); + Assert.That(res.invitedTeam[0].user.id, Is.EqualTo(_invitee.Account.userInfo.id)); + } + + [Test] + public async Task ProjectInviteGet() + { + var collaborator = await _invitee.ProjectInvite.Get(_project.id, _createdInvite.token); + + Assert.That( + collaborator, + Has.Property(nameof(PendingStreamCollaborator.inviteId)).EqualTo(_createdInvite.inviteId) + ); + Assert.That(collaborator.user.id, Is.EqualTo(_createdInvite.user.id)); + } + + [Test] + public async Task ProjectInviteUse_MemberAdded() + { + ProjectInviteUseInput input = new(true, _createdInvite.projectId, _createdInvite.token); + var res = await _invitee.ProjectInvite.Use(input); + Assert.That(res, Is.True); + + var project = await _inviter.Project.GetWithTeam(_project.id); + var teamMembers = project.team.Select(c => c.user.id); + var expectedTeamMembers = new[] { _inviter.Account.userInfo.id, _invitee.Account.userInfo.id }; + Assert.That(teamMembers, Is.EquivalentTo(expectedTeamMembers)); + } + + [Test] + public async Task ProjectInviteCancel_MemberNotAdded() + { + var res = await _inviter.ProjectInvite.Cancel(_createdInvite.projectId, _createdInvite.inviteId); + + Assert.That(res.invitedTeam, Is.Empty); + } + + [Test] + [TestCase(StreamRoles.STREAM_OWNER)] + [TestCase(StreamRoles.STREAM_REVIEWER)] + [TestCase(StreamRoles.STREAM_CONTRIBUTOR)] + [TestCase(StreamRoles.REVOKE)] + public async Task ProjectUpdateRole(string newRole) + { + await ProjectInviteUse_MemberAdded(); + ProjectUpdateRoleInput input = new(_invitee.Account.userInfo.id, _project.id, newRole); + _ = await _inviter.Project.UpdateRole(input); + + Project finalProject = await _invitee.Project.Get(_project.id); + Assert.That(finalProject.role, Is.EqualTo(newRole)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs new file mode 100644 index 0000000000..47222b761f --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceExceptionalTests.cs @@ -0,0 +1,113 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL; +using Speckle.Core.Api.GraphQL.Enums; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ProjectResource))] +public class ProjectResourceExceptionalTests +{ + private Client _testUser, + _secondUser, + _unauthedUser; + private Project _testProject; + private ProjectResource Sut => _testUser.Project; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _secondUser = await Fixtures.SeedUserWithClient(); + _unauthedUser = Fixtures.Unauthed; + _testProject = await _testUser.Project.Create(new("test project123", "desc", null)); + } + + //We want to check the following cases + // 1. User lacks permissions (without auth) + // 2. Target (Project or user) doesn't exist) + // 3. Cancellation + // 4. Server doesn't exist (is down) + //There's got to be a smarter way to parametrise these... + + [Test] + public void ProjectCreate_WithoutAuth() + { + ProjectCreateInput input = + new("The best project", "The best description for the best project", ProjectVisibility.Private); + + Assert.ThrowsAsync(async () => await _unauthedUser.Project.Create(input)); + } + + [Test] + public async Task ProjectGet_WithoutAuth() + { + ProjectCreateInput input = new("Private Stream", "A very private stream", ProjectVisibility.Private); + + Project privateStream = await Sut.Create(input); + + Assert.ThrowsAsync(async () => await _unauthedUser.Project.Get(privateStream.id)); + } + + [Test] + public void ProjectGet_NonExistentProject() + { + Assert.ThrowsAsync(async () => await Sut.Get("NonExistentProject")); + } + + [Test] + public void ProjectUpdate_NonExistentProject() + { + Assert.ThrowsAsync( + async () => _ = await Sut.Update(new("NonExistentProject", "My new name")) + ); + } + + [Test] + public void ProjectUpdate_NoAuth() + { + Assert.ThrowsAsync( + async () => _ = await _unauthedUser.Project.Update(new(_testProject.id, "My new name")) + ); + } + + [Test] + [TestCase(StreamRoles.STREAM_OWNER)] + [TestCase(StreamRoles.STREAM_CONTRIBUTOR)] + [TestCase(StreamRoles.STREAM_REVIEWER)] + [TestCase(StreamRoles.REVOKE)] + public void ProjectUpdateRole_NonExistentProject(string newRole) + { + ProjectUpdateRoleInput input = new(_secondUser.Account.id, "NonExistentProject", newRole); + + Assert.ThrowsAsync(async () => await Sut.UpdateRole(input)); + } + + [Test] + [TestCase(StreamRoles.STREAM_OWNER)] + [TestCase(StreamRoles.STREAM_CONTRIBUTOR)] + [TestCase(StreamRoles.STREAM_REVIEWER)] + [TestCase(StreamRoles.REVOKE)] + public void ProjectUpdateRole_NonAuth(string newRole) + { + ProjectUpdateRoleInput input = new(_secondUser.Account.id, "NonExistentProject", newRole); + Assert.ThrowsAsync(async () => await _unauthedUser.Project.UpdateRole(input)); + } + + [Test] + public async Task ProjectDelete_NonExistentProject() + { + bool response = await Sut.Delete(_testProject.id); + Assert.That(response, Is.True); + + Assert.ThrowsAsync(async () => _ = await Sut.Get(_testProject.id)); //TODO: Exception types + } + + [Test] + public void ProjectInvites_NoAuth() + { + Assert.ThrowsAsync(async () => await Fixtures.Unauthed.ActiveUser.ProjectInvites()); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceTests.cs new file mode 100644 index 0000000000..66dcba0a1e --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/ProjectResourceTests.cs @@ -0,0 +1,72 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Enums; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(ProjectResource))] +public class ProjectResourceTests +{ + private Client _testUser; + private Project _testProject; + private ProjectResource Sut => _testUser.Project; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _testProject = await _testUser.Project.Create(new("test project123", "desc", null)); + } + + [TestCase("Very private project", "My secret project", ProjectVisibility.Private)] + [TestCase("Very public project", null, ProjectVisibility.Public)] + public async Task ProjectCreate(string name, string desc, ProjectVisibility visibility) + { + ProjectCreateInput input = new(name, desc, visibility); + Project result = await Sut.Create(input); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Property(nameof(Project.id)).Not.Null); + Assert.That(result, Has.Property(nameof(Project.name)).EqualTo(input.name)); + Assert.That(result, Has.Property(nameof(Project.description)).EqualTo(input.description ?? string.Empty)); + Assert.That(result, Has.Property(nameof(Project.visibility)).EqualTo(input.visibility)); + } + + [Test] + public async Task ProjectGet() + { + Project result = await Sut.Get(_testProject.id); + + Assert.That(result.id, Is.EqualTo(_testProject.id)); + Assert.That(result.name, Is.EqualTo(_testProject.name)); + Assert.That(result.description, Is.EqualTo(_testProject.description)); + Assert.That(result.visibility, Is.EqualTo(_testProject.visibility)); + Assert.That(result.createdAt, Is.EqualTo(_testProject.createdAt)); + } + + [Test] + public async Task ProjectUpdate() + { + const string NEW_NAME = "MY new name"; + const string NEW_DESCRIPTION = "MY new desc"; + const ProjectVisibility NEW_VISIBILITY = ProjectVisibility.Public; + + Project newProject = await Sut.Update(new(_testProject.id, NEW_NAME, NEW_DESCRIPTION, null, NEW_VISIBILITY)); + + Assert.That(newProject.id, Is.EqualTo(_testProject.id)); + Assert.That(newProject.name, Is.EqualTo(NEW_NAME)); + Assert.That(newProject.description, Is.EqualTo(NEW_DESCRIPTION)); + Assert.That(newProject.visibility, Is.EqualTo(NEW_VISIBILITY)); + } + + [Test] + public async Task ProjectDelete() + { + Project toDelete = await Sut.Create(new("Delete me", null, null)); + bool response = await Sut.Delete(toDelete.id); + Assert.That(response, Is.True); + + Assert.ThrowsAsync(async () => _ = await Sut.Get(toDelete.id)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs new file mode 100644 index 0000000000..6e1e845c84 --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs @@ -0,0 +1,120 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(SubscriptionResource))] +public class SubscriptionResourceTests +{ + private const int WAIT_PERIOD = 300; + private Client _testUser; + private Project _testProject; + private Model _testModel; + private string _testVersion; + + private SubscriptionResource Sut => _testUser.Subscription; + + [OneTimeSetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _testProject = await _testUser.Project.Create(new("test project123", "desc", null)); + _testModel = await _testUser.Model.Create(new("test model", "desc", _testProject.id)); + _testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.name); + } + + [Test] + public async Task UserProjectsUpdated_SubscriptionIsCalled() + { + UserProjectsUpdatedMessage subscriptionMessage = null; + + using var sub = Sut.CreateUserProjectsUpdatedSubscription(); + sub.Listeners += (_, message) => subscriptionMessage = message; + + await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup + + var created = await _testUser.Project.Create(new(null, null, null)); + + await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered + + Assert.That(subscriptionMessage, Is.Not.Null); + Assert.That(subscriptionMessage.id, Is.EqualTo(created.id)); + } + + [Test] + public async Task ProjectModelsUpdated_SubscriptionIsCalled() + { + ProjectModelsUpdatedMessage subscriptionMessage = null; + + using var sub = Sut.CreateProjectModelsUpdatedSubscription(_testProject.id); + sub.Listeners += (_, message) => subscriptionMessage = message; + + await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup + + CreateModelInput input = new("my model", "myDescription", _testProject.id); + var created = await _testUser.Model.Create(input); + + await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered + + Assert.That(subscriptionMessage, Is.Not.Null); + Assert.That(subscriptionMessage.id, Is.EqualTo(created.id)); + } + + [Test] + public async Task ProjectUpdated_SubscriptionIsCalled() + { + ProjectUpdatedMessage subscriptionMessage = null; + + using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id); + sub.Listeners += (_, message) => subscriptionMessage = message; + + await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup + + var input = new ProjectUpdateInput(_testProject.id, "This is my new name"); + var created = await _testUser.Project.Update(input); + + await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered + + Assert.That(subscriptionMessage, Is.Not.Null); + Assert.That(subscriptionMessage.id, Is.EqualTo(created.id)); + } + + [Test] + public async Task ProjectVersionsUpdated_SubscriptionIsCalled() + { + ProjectVersionsUpdatedMessage subscriptionMessage = null; + + using var sub = Sut.CreateProjectVersionsUpdatedSubscription(_testProject.id); + sub.Listeners += (_, message) => subscriptionMessage = message; + + await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup + + var created = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.name); + + await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered + + Assert.That(subscriptionMessage, Is.Not.Null); + Assert.That(subscriptionMessage.id, Is.EqualTo(created)); + } + + [Test] + public async Task ProjectCommentsUpdated_SubscriptionIsCalled() + { + string resourceIdString = $"{_testProject.id},{_testModel.id},{_testVersion}"; + ProjectCommentsUpdatedMessage subscriptionMessage = null; + + using var sub = Sut.CreateProjectCommentsUpdatedSubscription(new(_testProject.id, resourceIdString)); + sub.Listeners += (_, message) => subscriptionMessage = message; + + await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup + + var created = await Fixtures.CreateComment(_testUser, _testProject.id, _testModel.id, _testVersion); + + await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered + + Assert.That(subscriptionMessage, Is.Not.Null); + Assert.That(subscriptionMessage.id, Is.EqualTo(created.id)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/VersionResourceTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/VersionResourceTests.cs new file mode 100644 index 0000000000..d54c966e0b --- /dev/null +++ b/Core/Tests/Speckle.Core.Tests.Integration/Api/GraphQL/Resources/VersionResourceTests.cs @@ -0,0 +1,117 @@ +using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; +using Speckle.Core.Api.GraphQL.Resources; +using Version = Speckle.Core.Api.GraphQL.Models.Version; + +namespace Speckle.Core.Tests.Integration.API.GraphQL.Resources; + +[TestOf(typeof(VersionResource))] +public class VersionResourceTests +{ + private Client _testUser; + private VersionResource Sut => _testUser.Version; + private Project _project; + private Model _model1; + private Model _model2; + private Version _version; + + [SetUp] + public async Task Setup() + { + _testUser = await Fixtures.SeedUserWithClient(); + _project = await _testUser.Project.Create(new("Test project", "", null)); + _model1 = await _testUser.Model.Create(new("Test Model 1", "", _project.id)); + _model2 = await _testUser.Model.Create(new("Test Model 2", "", _project.id)); + + string versionId = await Fixtures.CreateVersion(_testUser, _project.id, "Test Model 1"); + + _version = await Sut.Get(versionId, _model1.id, _project.id); + } + + [Test] + public async Task VersionGet() + { + Version result = await Sut.Get(_version.id, _model1.id, _project.id); + + Assert.That(result, Has.Property(nameof(Version.id)).EqualTo(_version.id)); + Assert.That(result, Has.Property(nameof(Version.message)).EqualTo(_version.message)); + } + + [Test] + public async Task VersionsGet() + { + ResourceCollection result = await Sut.GetVersions(_model1.id, _project.id); + + Assert.That(result.items, Has.Count.EqualTo(1)); + Assert.That(result.totalCount, Is.EqualTo(1)); + Assert.That(result.items[0], Has.Property(nameof(Version.id)).EqualTo(_version.id)); + } + + [Test] + public async Task VersionReceived() + { + CommitReceivedInput input = + new() + { + commitId = _version.id, + message = "we receieved it", + sourceApplication = "Integration test", + streamId = _project.id + }; + var result = await Sut.Received(input); + + Assert.That(result, Is.True); + } + + [Test] + public async Task ModelGetWithVersions() + { + Model result = await _testUser.Model.GetWithVersions(_model1.id, _project.id); + + Assert.That(result, Has.Property(nameof(Model.id)).EqualTo(_model1.id)); + Assert.That(result.versions.items, Has.Count.EqualTo(1)); + Assert.That(result.versions.totalCount, Is.EqualTo(1)); + Assert.That(result.versions.items[0], Has.Property(nameof(Version.id)).EqualTo(_version.id)); + } + + [Test] + public async Task VersionUpdate() + { + const string NEW_MESSAGE = "MY new version message"; + + UpdateVersionInput input = new(_version.id, NEW_MESSAGE); + Version updatedVersion = await Sut.Update(input); + + Assert.That(updatedVersion, Has.Property(nameof(Version.id)).EqualTo(_version.id)); + Assert.That(updatedVersion, Has.Property(nameof(Version.message)).EqualTo(NEW_MESSAGE)); + Assert.That(updatedVersion, Has.Property(nameof(Version.previewUrl)).EqualTo(_version.previewUrl)); + } + + [Test] + public async Task VersionMoveToModel() + { + MoveVersionsInput input = new(_model2.name, new[] { _version.id }); + string id = await Sut.MoveToModel(input); + Assert.That(id, Is.EqualTo(_model2.id)); + Version movedVersion = await Sut.Get(_version.id, _model2.id, _project.id); + + Assert.That(movedVersion, Has.Property(nameof(Version.id)).EqualTo(_version.id)); + Assert.That(movedVersion, Has.Property(nameof(Version.message)).EqualTo(_version.message)); + Assert.That(movedVersion, Has.Property(nameof(Version.previewUrl)).EqualTo(_version.previewUrl)); + + Assert.CatchAsync(async () => await Sut.Get(id, _model1.id, _project.id)); + } + + [Test] + public async Task VersionDelete() + { + DeleteVersionsInput input = new(new[] { _version.id }); + + bool response = await Sut.Delete(input); + Assert.That(response, Is.True); + + Assert.CatchAsync(async () => _ = await Sut.Get(_version.id, _model1.id, _project.id)); + Assert.CatchAsync(async () => _ = await Sut.Delete(input)); + } +} diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Credentials/UserServerInfoTests.cs b/Core/Tests/Speckle.Core.Tests.Integration/Credentials/UserServerInfoTests.cs index 0929c90926..2df5d5b3f5 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Credentials/UserServerInfoTests.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Credentials/UserServerInfoTests.cs @@ -1,5 +1,5 @@ using GraphQL.Client.Http; -using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; namespace Speckle.Core.Tests.Integration.Credentials; diff --git a/Core/Tests/Speckle.Core.Tests.Integration/Fixtures.cs b/Core/Tests/Speckle.Core.Tests.Integration/Fixtures.cs index 0e046f9bbf..7150dc38b8 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/Fixtures.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/Fixtures.cs @@ -4,9 +4,12 @@ using System.Web; using Newtonsoft.Json; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Inputs; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; using Speckle.Core.Logging; using Speckle.Core.Models; +using Speckle.Core.Transports; namespace Speckle.Core.Tests.Integration; @@ -23,7 +26,29 @@ public void BeforeAll() public static class Fixtures { - private static readonly ServerInfo s_server = new() { url = "http://localhost:3000", name = "Docker Server" }; + public static readonly ServerInfo Server = new() { url = "http://localhost:3000", name = "Docker Server" }; + + public static Client Unauthed => new Client(new Account { serverInfo = Server, userInfo = new UserInfo() }); + + public static async Task SeedUserWithClient() + { + return new Client(await SeedUser()); + } + + public static async Task CreateVersion(Client client, string projectId, string branchName) + { + using ServerTransport remote = new(client.Account, projectId); + var objectId = await Operations.Send(new() { applicationId = "ASDF" }, remote, false); + CommitCreateInput input = + new() + { + branchName = branchName, + message = "test version", + objectId = objectId, + streamId = projectId + }; + return await client.Version.Create(input); + } public static async Task SeedUser() { @@ -31,7 +56,7 @@ public static async Task SeedUser() Dictionary user = new() { - ["email"] = $"{seed.Substring(0, 7)}@acme.com", + ["email"] = $"{seed.Substring(0, 7)}@example.com", ["password"] = "12ABC3456789DEF0GHO", ["name"] = $"{seed.Substring(0, 5)} Name" }; @@ -40,7 +65,7 @@ public static async Task SeedUser() new HttpClientHandler { AllowAutoRedirect = false, CheckCertificateRevocationList = true } ); - httpClient.BaseAddress = new Uri(s_server.url); + httpClient.BaseAddress = new Uri(Server.url); string redirectUrl; try @@ -54,7 +79,7 @@ public static async Task SeedUser() } catch (Exception e) { - throw new Exception($"Cannot seed user on the server {s_server.url}", e); + throw new Exception($"Cannot seed user on the server {Server.url}", e); } Uri uri = new(redirectUrl); @@ -87,12 +112,11 @@ await tokenResponse.Content.ReadAsStringAsync() email = user["email"], name = user["name"] }, - serverInfo = s_server + serverInfo = Server }; - using var client = new Client(acc); - var user1 = await client.ActiveUserGet(); - acc.userInfo.id = user1.id; + var user1 = await AccountManager.GetUserInfo(acc.token, acc.serverInfo.url); + acc.userInfo = user1; return acc; } @@ -132,6 +156,23 @@ private static Blob GenerateBlob(string content) File.WriteAllText(filePath, content); return new Blob(filePath); } + + internal static async Task CreateComment(Client client, string projectId, string modelId, string versionId) + { + var blobs = await SendBlobData(client.Account, projectId); + var blobIds = blobs.Select(b => b.id).ToList(); + CreateCommentInput input = new(new(blobIds, null), projectId, $"{projectId},{modelId},{versionId}", null, null); + return await client.Comment.Create(input); + } + + internal static async Task SendBlobData(Account account, string projectId) + { + using ServerTransport remote = new(account, projectId); + var blobs = Fixtures.GenerateThreeBlobs(); + Base myObject = new() { ["blobs"] = blobs }; + await Operations.Send(myObject, remote, false); + return blobs; + } } public class UserIdResponse diff --git a/Core/Tests/Speckle.Core.Tests.Integration/GraphQLCLient.cs b/Core/Tests/Speckle.Core.Tests.Integration/GraphQLCLient.cs index afdb06caed..985aae175b 100644 --- a/Core/Tests/Speckle.Core.Tests.Integration/GraphQLCLient.cs +++ b/Core/Tests/Speckle.Core.Tests.Integration/GraphQLCLient.cs @@ -19,7 +19,7 @@ public async Task Setup() [Test] public void ThrowsForbiddenException() { - Assert.ThrowsAsync>>( + Assert.ThrowsAsync( async () => await _client.ExecuteGraphQLRequest>( new GraphQLRequest diff --git a/Core/Tests/Speckle.Core.Tests.Unit/Api/GraphQLClient.cs b/Core/Tests/Speckle.Core.Tests.Unit/Api/GraphQLClient.cs index 0733e39a85..9baadcac8b 100644 --- a/Core/Tests/Speckle.Core.Tests.Unit/Api/GraphQLClient.cs +++ b/Core/Tests/Speckle.Core.Tests.Unit/Api/GraphQLClient.cs @@ -2,6 +2,7 @@ using GraphQL; using NUnit.Framework; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; namespace Speckle.Core.Tests.Unit.Api; @@ -30,16 +31,10 @@ public void Dispose() private static IEnumerable ErrorCases() { + yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } }); + yield return new TestCaseData(typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } }); yield return new TestCaseData( - typeof(SpeckleGraphQLForbiddenException), - new Map { { "code", "FORBIDDEN" } } - ); - yield return new TestCaseData( - typeof(SpeckleGraphQLForbiddenException), - new Map { { "code", "UNAUTHENTICATED" } } - ); - yield return new TestCaseData( - typeof(SpeckleGraphQLInternalErrorException), + typeof(SpeckleGraphQLInternalErrorException), new Map { { "code", "INTERNAL_SERVER_ERROR" } } ); yield return new TestCaseData(typeof(SpeckleGraphQLException), new Map { { "foo", "bar" } }); @@ -109,7 +104,7 @@ public async Task TestExecuteWithResiliencePoliciesRetry() counter++; if (counter < maxRetryCount) { - throw new SpeckleGraphQLInternalErrorException(new GraphQLRequest(), new GraphQLResponse()); + throw new SpeckleGraphQLInternalErrorException(new GraphQLRequest(), new GraphQLResponse()); } return Task.FromResult(expectedResult); diff --git a/Core/Tests/Speckle.Core.Tests.Unit/Credentials/AccountServerMigrationTests.cs b/Core/Tests/Speckle.Core.Tests.Unit/Credentials/AccountServerMigrationTests.cs index 35c21adf7c..78c4f09330 100644 --- a/Core/Tests/Speckle.Core.Tests.Unit/Credentials/AccountServerMigrationTests.cs +++ b/Core/Tests/Speckle.Core.Tests.Unit/Credentials/AccountServerMigrationTests.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; namespace Speckle.Core.Tests.Unit.Credentials; @@ -14,12 +14,10 @@ public static IEnumerable MigrationTestCase() const string NEW_URL = "https://new.example.com"; const string OTHER_URL = "https://other.example.com"; Account oldAccount = CreateTestAccount(OLD_URL, null, new(NEW_URL)); - Account newAccount = CreateTestAccount(NEW_URL, new(OLD_URL), null); + string accountId = oldAccount.userInfo.id; // new account user must match old account user id + Account newAccount = CreateTestAccount(NEW_URL, new(OLD_URL), null, accountId); Account otherAccount = CreateTestAccount(OTHER_URL, null, null); - // new account user must match old account user id - newAccount.userInfo.id = oldAccount.userInfo.id; - List givenAccounts = new() { oldAccount, newAccount, otherAccount }; yield return new TestCaseData(givenAccounts, NEW_URL, new[] { newAccount }) @@ -59,8 +57,9 @@ public void TearDown() _accountsToCleanUp.Clear(); } - private static Account CreateTestAccount(string url, Uri movedFrom, Uri movedTo) + private static Account CreateTestAccount(string url, Uri movedFrom, Uri movedTo, string id = null) { + id ??= Guid.NewGuid().ToString(); return new Account { token = "myToken", @@ -72,7 +71,7 @@ private static Account CreateTestAccount(string url, Uri movedFrom, Uri movedTo) }, userInfo = new UserInfo { - id = Guid.NewGuid().ToString(), + id = id, email = "user@example.com", name = "user" } diff --git a/Core/Tests/Speckle.Core.Tests.Unit/Credentials/Accounts.cs b/Core/Tests/Speckle.Core.Tests.Unit/Credentials/Accounts.cs index 7c86a66f65..4ac91d4770 100644 --- a/Core/Tests/Speckle.Core.Tests.Unit/Credentials/Accounts.cs +++ b/Core/Tests/Speckle.Core.Tests.Unit/Credentials/Accounts.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; namespace Speckle.Core.Tests.Unit.Credentials; diff --git a/DesktopUI2/DesktopUI2/DummyBindings.cs b/DesktopUI2/DesktopUI2/DummyBindings.cs index d818645212..5b75c85c76 100644 --- a/DesktopUI2/DesktopUI2/DummyBindings.cs +++ b/DesktopUI2/DesktopUI2/DummyBindings.cs @@ -9,6 +9,7 @@ using DesktopUI2.Models.Settings; using DesktopUI2.ViewModels; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL; using Speckle.Core.Credentials; using Speckle.Core.Kits; using Speckle.Core.Models; @@ -224,21 +225,21 @@ public override List GetStreamsInFile() { id = "123", name = "Matteo Cominetti", - role = "stream:contributor", + role = StreamRoles.STREAM_CONTRIBUTOR, avatar = "https://avatars0.githubusercontent.com/u/2679513?s=88&v=4" }, new() { id = "321", name = "Izzy Lyseggen", - role = "stream:owner", + role = StreamRoles.STREAM_OWNER, avatar = "https://avatars2.githubusercontent.com/u/7717434?s=88&u=08db51f5799f6b21580485d915054b3582d519e6&v=4" }, new() { id = "456", name = "Dimitrie Stefanescu", - role = "stream:contributor", + role = StreamRoles.STREAM_CONTRIBUTOR, avatar = "https://avatars3.githubusercontent.com/u/7696515?s=88&u=fa253b5228d512e1ce79357c63925b7258e69f4c&v=4" } }; diff --git a/DesktopUI2/DesktopUI2/Utils.cs b/DesktopUI2/DesktopUI2/Utils.cs index 10057b4e38..d87a0f990c 100644 --- a/DesktopUI2/DesktopUI2/Utils.cs +++ b/DesktopUI2/DesktopUI2/Utils.cs @@ -19,6 +19,7 @@ using Material.Dialog.Interfaces; using SkiaSharp; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; using Speckle.Core.Logging; diff --git a/DesktopUI2/DesktopUI2/ViewModels/AccountViewModel.cs b/DesktopUI2/DesktopUI2/ViewModels/AccountViewModel.cs index d1e5154642..f9faee7727 100644 --- a/DesktopUI2/DesktopUI2/ViewModels/AccountViewModel.cs +++ b/DesktopUI2/DesktopUI2/ViewModels/AccountViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Media.Imaging; using ReactiveUI; using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; using Speckle.Core.Credentials; using Speckle.Core.Helpers; using Speckle.Core.Logging; diff --git a/DesktopUI2/DesktopUI2/ViewModels/NotificationViewModel.cs b/DesktopUI2/DesktopUI2/ViewModels/NotificationViewModel.cs index 816e36c755..eb9cf10829 100644 --- a/DesktopUI2/DesktopUI2/ViewModels/NotificationViewModel.cs +++ b/DesktopUI2/DesktopUI2/ViewModels/NotificationViewModel.cs @@ -3,7 +3,7 @@ using Avalonia.Media; using Material.Icons; using ReactiveUI; -using Speckle.Core.Api; +using Speckle.Core.Api.GraphQL.Models; namespace DesktopUI2.ViewModels;