diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index bbb2f726a..8dd41a223 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -45,6 +45,9 @@ jobs: PN_PUB_KEY: ${{ secrets.SDK_PUB_KEY }} PN_SUB_KEY: ${{ secrets.SDK_SUB_KEY }} PN_SEC_KEY: ${{ secrets.SDK_SEC_KEY }} + PN_DS_PUB_KEY: ${{ secrets.SDK_DS_PUB_KEY }} + PN_DS_SUB_KEY: ${{ secrets.SDK_DS_SUB_KEY }} + PN_DS_SEC_KEY: ${{ secrets.SDK_DS_SEC_KEY }} PN_PAM_PUB_KEY: ${{ secrets.SDK_PAM_PUB_KEY }} PN_PAM_SUB_KEY: ${{ secrets.SDK_PAM_SUB_KEY }} PN_PAM_SEC_KEY: ${{ secrets.SDK_PAM_SEC_KEY }} diff --git a/.pubnub.yml b/.pubnub.yml index e120d4739..1f63858c1 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,8 +1,13 @@ name: c-sharp -version: "8.3.4" +version: "9.0.0" schema: 1 scm: github.com/pubnub/c-sharp changelog: + - date: 2026-09-08 + version: v9.0.0 + changes: + - type: feature + text: "Added Data Sync feature support." - date: 2026-07-29 version: v8.3.4 changes: @@ -1003,6 +1008,41 @@ features: - OBJECTS-MANAGE-CHANNEL-MEMBERS-V2 - OBJECTS-FILTERING - OBJECTS-SORTING + data-sync: + - DATASYNC-USER-CREATE + - DATASYNC-USER-GET + - DATASYNC-USER-GET-ALL + - DATASYNC-USER-UPDATE + - DATASYNC-USER-PATCH + - DATASYNC-USER-DELETE + - DATASYNC-CHANNEL-CREATE + - DATASYNC-CHANNEL-GET + - DATASYNC-CHANNEL-GET-ALL + - DATASYNC-CHANNEL-UPDATE + - DATASYNC-CHANNEL-PATCH + - DATASYNC-CHANNEL-DELETE + - DATASYNC-MEMBERSHIP-CREATE + - DATASYNC-MEMBERSHIP-GET + - DATASYNC-MEMBERSHIP-GET-ALL + - DATASYNC-MEMBERSHIP-UPDATE + - DATASYNC-MEMBERSHIP-PATCH + - DATASYNC-MEMBERSHIP-DELETE + - DATASYNC-ENTITY-CREATE + - DATASYNC-ENTITY-GET + - DATASYNC-ENTITY-GET-ALL + - DATASYNC-ENTITY-UPDATE + - DATASYNC-ENTITY-PATCH + - DATASYNC-ENTITY-DELETE + - DATASYNC-RELATIONSHIP-CREATE + - DATASYNC-RELATIONSHIP-GET + - DATASYNC-RELATIONSHIP-GET-ALL + - DATASYNC-RELATIONSHIP-UPDATE + - DATASYNC-RELATIONSHIP-PATCH + - DATASYNC-RELATIONSHIP-DELETE + - DATASYNC-FILTERING + - DATASYNC-SORTING + - DATASYNC-EVENTS-LISTENER + - DATASYNC-ACCESS-MANAGER files: - FILES-SEND-FILE - FILES-LIST-FILES @@ -1018,14 +1058,14 @@ features: - QUERY-PARAM supported-platforms: - - version: Pubnub 'C#' 8.3.4 + version: Pubnub 'C#' 9.0.0 platforms: - Windows 10 and up - Windows Server 2008 and up frameworks: - .Net Framework 4.5+ - - version: PubnubPCL 'C#' 8.3.4 + version: PubnubPCL 'C#' 9.0.0 platforms: - Xamarin.Android - Xamarin.iOS @@ -1037,7 +1077,7 @@ supported-platforms: frameworks: - .Net 4.5+ - - version: PubnubUWP 'C#' 8.3.4 + version: PubnubUWP 'C#' 9.0.0 platforms: - Windows Phone 10 - Universal Windows Apps @@ -1061,7 +1101,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: Pubnub - location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4 + location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0 requires: - name: ".Net" @@ -1302,7 +1342,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: PubNubPCL - location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4 + location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0 requires: - name: ".Net" @@ -1653,7 +1693,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: PubnubUWP - location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4 + location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0 requires: - name: "Universal Windows Platform Development" diff --git a/CHANGELOG b/CHANGELOG index 3eb1bdd40..8e6d3ec22 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +v9.0.0 - September 08 2026 +----------------------------- +- Added: added Data Sync feature support. + v8.3.4 - July 29 2026 ----------------------------- - Fixed: fixed issue related to Reconnect to retain subscription state. diff --git a/src/Api/PubnubApi/Callbacks/SubscribeCallback.cs b/src/Api/PubnubApi/Callbacks/SubscribeCallback.cs index 996f0cb5d..333571bb0 100644 --- a/src/Api/PubnubApi/Callbacks/SubscribeCallback.cs +++ b/src/Api/PubnubApi/Callbacks/SubscribeCallback.cs @@ -14,6 +14,8 @@ public abstract class SubscribeCallback public abstract void ObjectEvent(Pubnub pubnub, PNObjectEventResult objectEvent); + public virtual void DataSyncEvent(Pubnub pubnub, PNDataSyncEventResult dataSyncEvent) { } + public abstract void MessageAction(Pubnub pubnub, PNMessageActionEventResult messageAction); public abstract void File(Pubnub pubnub, PNFileEventResult fileEvent); diff --git a/src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs b/src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs index 866868b7c..b2efcc99e 100644 --- a/src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs +++ b/src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs @@ -29,12 +29,21 @@ public class GrantTokenOperation : PubnubCoreBase Users = new Dictionary() }; + private const string UuidsUsersConflictMessage = "Either Uuids or Users can be used. Not both."; + + // Channels/Spaces and Uuids/Users are alternative names for the same token scopes, so a + // token can only carry one name from each pair. When both are supplied across Resources + // and Patterns the classic name wins and the alternative is left out of the request. + private const string SpacesIgnoredMessage = "GrantToken: Spaces permissions were supplied together with Channels permissions, so the Spaces permissions were ignored and are NOT included in the token. Channels and Spaces are alternative names for the same scope - supply only one of them across Resources and Patterns."; + private const string UsersIgnoredMessage = "GrantToken: Users permissions were supplied together with Uuids permissions, so the Users permissions were ignored and are NOT included in the token. Uuids and Users are alternative names for the same scope - supply only one of them across Resources and Patterns."; + private int grantTTL = -1; private PNCallback savedCallbackGrantToken; private Dictionary queryParam; private Dictionary grantMeta; private string pubnubAuthorizedUuid = string.Empty; private string pubnubAuthorizedUserId = string.Empty; + private PNDataSyncProjections pubnubDataSyncProjections; public GrantTokenOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, IPubnubUnitTest pubnubUnit, EndPoint.TokenManager tokenManager, Pubnub instance) : base(pubnubConfig, jsonPluggableLibrary, pubnubUnit, tokenManager, instance) { @@ -71,9 +80,8 @@ public GrantTokenOperation Resources(PNTokenResources resources) resources.Spaces != null && resources.Spaces.Count > 0) { throw new ArgumentException("Either Channels or Spaces can be used. Not both."); } - if (resources.Uuids != null && resources.Uuids.Count > 0 && - resources.Users != null && resources.Users.Count > 0) { - throw new ArgumentException("Either Uuids or Users can be used. Not both."); + if (HasEntries(resources.Uuids) && HasEntries(resources.Users)) { + throw new ArgumentException(UuidsUsersConflictMessage); } pubnubResources = resources; if (pubnubResources.Channels == null) { @@ -102,9 +110,8 @@ public GrantTokenOperation Patterns(PNTokenPatterns patterns) patterns.Spaces != null && patterns.Spaces.Count > 0) { throw new ArgumentException("Either Channels or Spaces can be used. Not both."); } - if (patterns.Uuids != null && patterns.Uuids.Count > 0 && - patterns.Users != null && patterns.Users.Count > 0) { - throw new ArgumentException("Either Uuids or Users can be used. Not both."); + if (HasEntries(patterns.Uuids) && HasEntries(patterns.Users)) { + throw new ArgumentException(UuidsUsersConflictMessage); } pubnubPatterns = patterns; @@ -128,6 +135,17 @@ public GrantTokenOperation Patterns(PNTokenPatterns patterns) return this; } + /// + /// Assigns Data Sync projections to resources/patterns. The projection name + /// per resource is encoded into the token's meta "pn-projections" section. + /// Entity-level Data Sync permissions are set through / . + /// + public GrantTokenOperation DataSyncProjections(PNDataSyncProjections projections) + { + this.pubnubDataSyncProjections = projections; + return this; + } + public GrantTokenOperation TTL(int ttl) { this.grantTTL = ttl; @@ -214,6 +232,11 @@ internal void GrantAccess(PNCallback callback) }); } + private static bool HasEntries(Dictionary perms) + { + return perms != null && perms.Count > 0; + } + private bool FillPermissionMappingWithMaskValues(Dictionary dPerms, bool currentAtleastOnePermission, out Dictionary dPermsWithMaskValues) { dPermsWithMaskValues = new Dictionary(); @@ -230,6 +253,72 @@ private bool FillPermissionMappingWithMaskValues(Dictionary collection, PNDataSyncTokenScopes scopes, bool currentAtleastOnePermission) + { + bool atleastOnePermission = currentAtleastOnePermission; + if (scopes == null) { + return atleastOnePermission; + } + atleastOnePermission = AddDataSyncScope(collection, "datasync:entities", scopes.Entities, atleastOnePermission); + atleastOnePermission = AddDataSyncScope(collection, "datasync:relationships", scopes.Relationships, atleastOnePermission); + atleastOnePermission = AddDataSyncScope(collection, "datasync:memberships", scopes.Memberships, atleastOnePermission); + return atleastOnePermission; + } + + private bool AddDataSyncScope(Dictionary collection, string key, Dictionary perms, bool currentAtleastOnePermission) + { + if (perms == null || perms.Count == 0) { + return currentAtleastOnePermission; + } + bool atleastOnePermission = FillPermissionMappingWithMaskValues(perms, currentAtleastOnePermission, out Dictionary masks); + collection[key] = masks; + return atleastOnePermission; + } + + private Dictionary BuildProjectionsMeta() + { + if (this.pubnubDataSyncProjections == null) { + return null; + } + Dictionary pnProjections = new Dictionary(); + Dictionary res = BuildProjectionScope(this.pubnubDataSyncProjections.Resources); + Dictionary pat = BuildProjectionScope(this.pubnubDataSyncProjections.Patterns); + if (res != null) { + pnProjections["res"] = res; + } + if (pat != null) { + pnProjections["pat"] = pat; + } + return pnProjections.Count > 0 ? pnProjections : null; + } + + private static Dictionary BuildProjectionScope(PNDataSyncProjectionScope scope) + { + if (scope == null) { + return null; + } + Dictionary flat = new Dictionary(); + FlattenProjections(flat, scope.Entities, "entities"); + FlattenProjections(flat, scope.Users, "users"); + FlattenProjections(flat, scope.Channels, "channels"); + FlattenProjections(flat, scope.Relationships, "relationships"); + FlattenProjections(flat, scope.Memberships, "memberships"); + return flat.Count > 0 ? flat : null; + } + + private static void FlattenProjections(Dictionary target, Dictionary map, string type) + { + if (map == null) { + return; + } + foreach (KeyValuePair kvp in map) { + if (string.IsNullOrEmpty(kvp.Key)) { + continue; + } + target[$"datasync:{type}:{kvp.Key}"] = kvp.Value; + } + } + internal async Task> GrantAccess() { if (string.IsNullOrEmpty(config.SecretKey) || string.IsNullOrEmpty(config.SecretKey.Trim()) || config.SecretKey.Length <= 0) { @@ -341,6 +430,9 @@ private RequestParameter CreateRequestParameter() atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubResources.Spaces, atleastOnePermission, out spBitmaskPermCollection); atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubPatterns.Spaces, atleastOnePermission, out spPatternBitmaskPermCollection); } else { + if (HasEntries(pubnubResources.Spaces) || HasEntries(pubnubPatterns.Spaces)) { + config?.Logger?.Warn(SpacesIgnoredMessage); + } spBitmaskPermCollection = new Dictionary(); spPatternBitmaskPermCollection = new Dictionary(); } @@ -363,14 +455,13 @@ private RequestParameter CreateRequestParameter() atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubResources.Users, atleastOnePermission, out userBitmaskPermCollection); atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubPatterns.Users, atleastOnePermission, out userPatternBitmaskPermCollection); } else { + if (HasEntries(pubnubResources.Users) || HasEntries(pubnubPatterns.Users)) { + config?.Logger?.Warn(UsersIgnoredMessage); + } userBitmaskPermCollection = new Dictionary(); userPatternBitmaskPermCollection = new Dictionary(); } - if (!atleastOnePermission) { - config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces or groups"); - } - Dictionary resourcesCollection = new Dictionary { { "channels", chBitmaskPermCollection }, @@ -389,11 +480,23 @@ private RequestParameter CreateRequestParameter() { "spaces", spPatternBitmaskPermCollection } }; + atleastOnePermission = AddDataSyncScopes(resourcesCollection, this.pubnubResources.DataSync, atleastOnePermission); + atleastOnePermission = AddDataSyncScopes(patternsCollection, this.pubnubPatterns.DataSync, atleastOnePermission); + + if (!atleastOnePermission) { + config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces, groups or datasync entities/relationships/memberships"); + } + Dictionary optimizedMeta = new Dictionary(); if (this.grantMeta != null) { optimizedMeta = this.grantMeta; } + Dictionary pnProjections = BuildProjectionsMeta(); + if (pnProjections != null) { + optimizedMeta["pn-projections"] = pnProjections; + } + Dictionary permissionCollection = new Dictionary { { "resources", resourcesCollection }, diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelOperation.cs new file mode 100644 index 000000000..b558befe2 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelOperation.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class CreateChannelOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly CreateChannelParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncCreateChannel; + + public CreateChannelOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + CreateChannelParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await CreateChannelAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> CreateChannelAsync() + { + var returnValue = new PNResult(); + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = true + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == 201) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, 201, + null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.Id)) + { + dataProperties.Add("id", parameters.Id); + } + + if (!string.IsNullOrEmpty(parameters.EntityClass)) + { + dataProperties.Add("entityClass", parameters.EntityClass); + } + + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + dataProperties.Add("entityClassLevel", parameters.EntityClassLevel); + } + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var postBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels" + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.POST, + PathSegment = pathSegments, + BodyContentString = postBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.channel+json;version=1"); + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelParameters.cs new file mode 100644 index 000000000..638d6ce58 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/CreateChannelParameters.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class CreateChannelParameters + { + /// + /// Channel identifier. Optional — if not provided, the server generates one. + /// Must be 1–255 characters if provided. + /// + public string Id { get; set; } + + /// + /// Entity class name. Optional — defaults to "Channel" on the server if not + /// provided. Must be "Channel" or a subclass of "Channel". + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// + public int EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// Channel status (e.g., "active", "archived"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// + public Dictionary Payload { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelOperation.cs new file mode 100644 index 000000000..62015eca5 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelOperation.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class DeleteChannelOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly DeleteChannelParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncDeleteChannel; + + public DeleteChannelOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + DeleteChannelParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await DeleteChannelAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> DeleteChannelAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Channel Id", + new ArgumentException("Missing Channel Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + logger?.Trace($"{GetType().Name} request finished with status code {transportResponse.StatusCode}"); + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + returnValue.Result = new PNDataSyncDeleteChannelResult(); + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.DELETE, + PathSegment = pathSegments + }; + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelParameters.cs new file mode 100644 index 000000000..dc3d6d918 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/DeleteChannelParameters.cs @@ -0,0 +1,16 @@ +namespace PubnubApi.EndPoint +{ + public class DeleteChannelParameters + { + /// + /// Channel identifier. Required. + /// + public string Id { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the delete when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelOperation.cs new file mode 100644 index 000000000..16c1b80e6 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelOperation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetChannelOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetChannelParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetChannel; + + public GetChannelOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetChannelParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetChannelAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetChannelAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Channel Id", + new ArgumentException("Missing Channel Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelParameters.cs new file mode 100644 index 000000000..02b016b1a --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelParameters.cs @@ -0,0 +1,10 @@ +namespace PubnubApi.EndPoint +{ + public class GetChannelParameters + { + /// + /// Channel identifier. Required. + /// + public string Id { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsOperation.cs new file mode 100644 index 000000000..304d52f00 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsOperation.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetChannelsOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetChannelsParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetChannels; + + public GetChannelsOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetChannelsParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetChannelsAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetChannelsAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels" + }; + + var requestQueryStringParams = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.EntityClass)) + { + requestQueryStringParams.Add("entity_class", + UriUtil.EncodeUriComponent(parameters.EntityClass, + OperationType, false, false, false)); + } + + if (parameters.EntityClassVersion.HasValue) + { + requestQueryStringParams.Add("entity_class_version", + parameters.EntityClassVersion.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + requestQueryStringParams.Add("entity_class_level", + UriUtil.EncodeUriComponent(parameters.EntityClassLevel, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Cursor)) + { + requestQueryStringParams.Add("cursor", + UriUtil.EncodeUriComponent(parameters.Cursor, + OperationType, false, false, false)); + } + + if (parameters.Limit.HasValue) + { + requestQueryStringParams.Add("limit", + parameters.Limit.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.FilterFast)) + { + requestQueryStringParams.Add("filter_fast", + UriUtil.EncodeUriComponent(parameters.FilterFast, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Filter)) + { + requestQueryStringParams.Add("filter", + UriUtil.EncodeUriComponent(parameters.Filter, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Sort)) + { + requestQueryStringParams.Add("sort", + UriUtil.EncodeUriComponent(parameters.Sort, + OperationType, false, false, false)); + } + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments, + Query = requestQueryStringParams + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsParameters.cs new file mode 100644 index 000000000..0fc648c96 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/GetChannelsParameters.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class GetChannelsParameters + { + /// + /// Entity class name to filter by. Optional — if not provided the server returns + /// channels of every class descending from "Channel". + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Optional — if not provided the server + /// returns channels matching the latest version. + /// + public int? EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// Pagination cursor returned from a previous request. + /// + public string Cursor { get; set; } + + /// + /// Maximum number of items to return per page. + /// Min 1, max 100, default 20. + /// + public int? Limit { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against strongly + /// consistent storage. Supports a limited number of conditions. + /// Example: "status == 'active'". + /// + public string FilterFast { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against eventually + /// consistent storage. Supports logical operators and nested conditions. + /// + public string Filter { get; set; } + + /// + /// Comma-separated list of fields to sort by, each optionally suffixed with + /// :desc (ascending by default). Example: "createdAt:desc,id". + /// + public string Sort { get; set; } + } + + public class PNDataSyncChannelsListResult + { + public List Data { get; internal set; } = new(); + public PaginationMeta Meta { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncChannelResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncChannelResult.cs new file mode 100644 index 000000000..9828b6ad9 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncChannelResult.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint; + +public class PNDataSyncChannelResult +{ + public string Id { get; internal set; } + public string EntityClass { get; internal set; } + public int EntityClassVersion { get; internal set; } + public string EntityClassLevel { get; internal set; } + public string Status { get; internal set; } + public Dictionary Payload { get; internal set; } + public string CreatedAt { get; internal set; } + public string UpdatedAt { get; internal set; } + public string ETag { get; internal set; } + public string ExpiresAt { get; internal set; } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncDeleteChannelResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncDeleteChannelResult.cs new file mode 100644 index 000000000..f1169ac68 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/PNDataSyncDeleteChannelResult.cs @@ -0,0 +1,5 @@ +namespace PubnubApi.EndPoint; + +public class PNDataSyncDeleteChannelResult +{ +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelOperation.cs new file mode 100644 index 000000000..a2b3e362d --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class SetChannelOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly SetChannelParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncUpdateChannel; + + public SetChannelOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + SetChannelParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await UpdateChannelAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> UpdateChannelAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Channel Id", + new ArgumentException("Missing Channel Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var putBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PUT, + PathSegment = pathSegments, + BodyContentString = putBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.channel+json;version=1"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelParameters.cs new file mode 100644 index 000000000..855f6a2d5 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/SetChannelParameters.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class SetChannelParameters + { + /// + /// Channel identifier. Required. + /// + public string Id { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// + public int EntityClassVersion { get; set; } + + /// + /// Channel status (e.g., "active", "archived"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// Replaces the entire payload — omitted fields are removed. + /// + public Dictionary Payload { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the update when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelOperation.cs new file mode 100644 index 000000000..04f8f711d --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelOperation.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class UpdateChannelOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly UpdateChannelParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncPatchChannel; + + private static readonly HashSet OpsRequiringValue = + new () { JsonPatchOperationType.Add, JsonPatchOperationType.Replace, JsonPatchOperationType.Test }; + + private static readonly HashSet OpsRequiringFrom = + new () { JsonPatchOperationType.Move, JsonPatchOperationType.Copy }; + + public UpdateChannelOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + UpdateChannelParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await PatchChannelAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> PatchChannelAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Channel Id", + new ArgumentException("Missing Channel Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.Operations == null || parameters.Operations.Count == 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Operations list must contain at least one operation", + new ArgumentException("Operations list must contain at least one operation")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var patchArray = new List>(); + + foreach (var op in parameters.Operations) + { + var entry = new Dictionary + { + { "op", op.Op.ToString().ToLowerInvariant() }, + { "path", op.Path } + }; + + if (OpsRequiringValue.Contains(op.Op)) + { + entry["value"] = op.Value; + } + + if (OpsRequiringFrom.Contains(op.Op) && !string.IsNullOrEmpty(op.From)) + { + entry["from"] = op.From; + } + + patchArray.Add(entry); + } + + var patchBody = jsonLibrary.SerializeToJsonString(patchArray); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "channels", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PATCH, + PathSegment = pathSegments, + BodyContentString = patchBody + }; + + requestParameter.Headers.Add("Content-Type", "application/json-patch+json"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelParameters.cs new file mode 100644 index 000000000..cbaaa61f8 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Channel/UpdateChannelParameters.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class UpdateChannelParameters + { + /// + /// Channel identifier. Required. + /// + public string Id { get; set; } + + /// + /// List of JSON Patch operations (RFC 6902) to apply. + /// + public List Operations { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the patch when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/DataSync.cs b/src/Api/PubnubApi/EndPoint/DataSync/DataSync.cs new file mode 100644 index 000000000..645948593 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/DataSync.cs @@ -0,0 +1,386 @@ +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint; + +/// +/// Entrypoint for PubNub Data Sync operations +/// +public class DataSync +{ + private readonly IPubnubUnitTest unit; + private readonly Pubnub pubnub; + private readonly TokenManager tokenManager; + + public DataSync(Pubnub pubnub, IPubnubUnitTest unit, TokenManager tokenManager) + { + this.pubnub = pubnub; + this.unit = unit; + this.tokenManager = tokenManager; + } + + public async Task> CreateEntity(CreateEntityParameters parameters) + { + return await new CreateEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void CreateEntity(CreateEntityParameters parameters, PNDataSyncEntityResultExt callback) + { + new CreateEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetEntity(GetEntityParameters parameters) + { + return await new GetEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetEntity(GetEntityParameters parameters, PNDataSyncEntityResultExt callback) + { + new GetEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetEntities(GetEntitiesParameters parameters) + { + return await new GetEntitiesOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetEntities(GetEntitiesParameters parameters, PNDataSyncEntitiesListResultExt callback) + { + new GetEntitiesOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> SetEntity(SetEntityParameters parameters) + { + return await new SetEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void SetEntity(SetEntityParameters parameters, PNDataSyncEntityResultExt callback) + { + new SetEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> UpdateEntity(UpdateEntityParameters parameters) + { + return await new UpdateEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void UpdateEntity(UpdateEntityParameters parameters, PNDataSyncEntityResultExt callback) + { + new UpdateEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> DeleteEntity(DeleteEntityParameters parameters) + { + return await new DeleteEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void DeleteEntity(DeleteEntityParameters parameters, PNDataSyncDeleteEntityResultExt callback) + { + new DeleteEntityOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> CreateRelationship(CreateRelationshipParameters parameters) + { + return await new CreateRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void CreateRelationship(CreateRelationshipParameters parameters, PNDataSyncRelationshipResultExt callback) + { + new CreateRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetRelationship(GetRelationshipParameters parameters) + { + return await new GetRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetRelationship(GetRelationshipParameters parameters, PNDataSyncRelationshipResultExt callback) + { + new GetRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetRelationships(GetRelationshipsParameters parameters) + { + return await new GetRelationshipsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetRelationships(GetRelationshipsParameters parameters, PNDataSyncRelationshipsListResultExt callback) + { + new GetRelationshipsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> SetRelationship(SetRelationshipParameters parameters) + { + return await new SetRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void SetRelationship(SetRelationshipParameters parameters, PNDataSyncRelationshipResultExt callback) + { + new SetRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> UpdateRelationship(UpdateRelationshipParameters parameters) + { + return await new UpdateRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void UpdateRelationship(UpdateRelationshipParameters parameters, PNDataSyncRelationshipResultExt callback) + { + new UpdateRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> DeleteRelationship(DeleteRelationshipParameters parameters) + { + return await new DeleteRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void DeleteRelationship(DeleteRelationshipParameters parameters, PNDataSyncDeleteRelationshipResultExt callback) + { + new DeleteRelationshipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + // User operations + + public async Task> CreateUser(CreateUserParameters parameters) + { + return await new CreateUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void CreateUser(CreateUserParameters parameters, PNDataSyncUserResultExt callback) + { + new CreateUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetUser(GetUserParameters parameters) + { + return await new GetUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetUser(GetUserParameters parameters, PNDataSyncUserResultExt callback) + { + new GetUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetUsers(GetUsersParameters parameters) + { + return await new GetUsersOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetUsers(GetUsersParameters parameters, PNDataSyncUsersListResultExt callback) + { + new GetUsersOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> SetUser(SetUserParameters parameters) + { + return await new SetUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void SetUser(SetUserParameters parameters, PNDataSyncUserResultExt callback) + { + new SetUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> UpdateUser(UpdateUserParameters parameters) + { + return await new UpdateUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void UpdateUser(UpdateUserParameters parameters, PNDataSyncUserResultExt callback) + { + new UpdateUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> DeleteUser(DeleteUserParameters parameters) + { + return await new DeleteUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void DeleteUser(DeleteUserParameters parameters, PNDataSyncDeleteUserResultExt callback) + { + new DeleteUserOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + // Channel operations + + public async Task> CreateChannel(CreateChannelParameters parameters) + { + return await new CreateChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void CreateChannel(CreateChannelParameters parameters, PNDataSyncChannelResultExt callback) + { + new CreateChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetChannel(GetChannelParameters parameters) + { + return await new GetChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetChannel(GetChannelParameters parameters, PNDataSyncChannelResultExt callback) + { + new GetChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetChannels(GetChannelsParameters parameters) + { + return await new GetChannelsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetChannels(GetChannelsParameters parameters, PNDataSyncChannelsListResultExt callback) + { + new GetChannelsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> SetChannel(SetChannelParameters parameters) + { + return await new SetChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void SetChannel(SetChannelParameters parameters, PNDataSyncChannelResultExt callback) + { + new SetChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> UpdateChannel(UpdateChannelParameters parameters) + { + return await new UpdateChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void UpdateChannel(UpdateChannelParameters parameters, PNDataSyncChannelResultExt callback) + { + new UpdateChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> DeleteChannel(DeleteChannelParameters parameters) + { + return await new DeleteChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void DeleteChannel(DeleteChannelParameters parameters, PNDataSyncDeleteChannelResultExt callback) + { + new DeleteChannelOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + // Membership operations + + public async Task> CreateMembership(CreateMembershipParameters parameters) + { + return await new CreateMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void CreateMembership(CreateMembershipParameters parameters, PNDataSyncMembershipResultExt callback) + { + new CreateMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetMembership(GetMembershipParameters parameters) + { + return await new GetMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetMembership(GetMembershipParameters parameters, PNDataSyncMembershipResultExt callback) + { + new GetMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> GetMemberships(GetMembershipsParameters parameters) + { + return await new GetDataSyncMembershipsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void GetMemberships(GetMembershipsParameters parameters, PNDataSyncMembershipsListResultExt callback) + { + new GetDataSyncMembershipsOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> SetMembership(UpdateMembershipParameters parameters) + { + return await new SetMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void SetMembership(UpdateMembershipParameters parameters, PNDataSyncMembershipResultExt callback) + { + new SetMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> UpdateMembership(PatchMembershipParameters parameters) + { + return await new UpdateMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void UpdateMembership(PatchMembershipParameters parameters, PNDataSyncMembershipResultExt callback) + { + new UpdateMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } + + public async Task> DeleteMembership(DeleteMembershipParameters parameters) + { + return await new DeleteMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).ExecuteAsync(); + } + + public void DeleteMembership(DeleteMembershipParameters parameters, PNDataSyncDeleteMembershipResultExt callback) + { + new DeleteMembershipOperation(pubnub.PNConfig, pubnub.JsonPluggableLibrary, unit, tokenManager, pubnub, + parameters).Execute(callback); + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityOperation.cs new file mode 100644 index 000000000..a66c3d078 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityOperation.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class CreateEntityOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly CreateEntityParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncCreateEntity; + + public CreateEntityOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + CreateEntityParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await CreateEntityAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> CreateEntityAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.EntityClass) || string.IsNullOrEmpty(parameters.EntityClass.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing EntityClass", + new ArgumentException("Missing EntityClass")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = true + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + //TODO: why 201 and not 200 from constants? + if (errorStatus == null && transportResponse.StatusCode == 201) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, 201, + null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.Id)) + { + dataProperties.Add("id", parameters.Id); + } + + dataProperties.Add("entityClass", parameters.EntityClass); + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + dataProperties.Add("entityClassLevel", parameters.EntityClassLevel); + } + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var postBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities" + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.POST, + PathSegment = pathSegments, + BodyContentString = postBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.entity+json;version=1"); + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityParameters.cs new file mode 100644 index 000000000..690ef3742 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/CreateEntityParameters.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for creating a new entity via DataSync. + /// + public class CreateEntityParameters + { + /// + /// Entity identifier. Optional — if not provided, the server generates one. + /// Must be 1–255 characters if provided. + /// + public string Id { get; set; } + + /// + /// Entity class identifier (e.g., "vehicle", "order", "sensor"). + /// Required. Immutable after creation. + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// + public int EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// Entity status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// + public Dictionary Payload { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityOperation.cs new file mode 100644 index 000000000..9750b0d6a --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityOperation.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class DeleteEntityOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly DeleteEntityParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncDeleteEntity; + + public DeleteEntityOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + DeleteEntityParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await DeleteEntityAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> DeleteEntityAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Entity Id", + new ArgumentException("Missing Entity Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + logger?.Trace($"{GetType().Name} request finished with status code {transportResponse.StatusCode}"); + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + returnValue.Result = new PNDataSyncDeleteEntityResult(); + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.DELETE, + PathSegment = pathSegments + }; + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityParameters.cs new file mode 100644 index 000000000..926a6025e --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/DeleteEntityParameters.cs @@ -0,0 +1,19 @@ +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for deleting an entity by ID via DataSync. + /// + public class DeleteEntityParameters + { + /// + /// Entity identifier. Required. + /// + public string Id { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the delete when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesOperation.cs new file mode 100644 index 000000000..683b9e2ae --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesOperation.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetEntitiesOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetEntitiesParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetEntities; + + public GetEntitiesOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetEntitiesParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetEntitiesAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetEntitiesAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.EntityClass) || string.IsNullOrEmpty(parameters.EntityClass.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing EntityClass", + new ArgumentException("Missing EntityClass")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities" + }; + + var requestQueryStringParams = new Dictionary(); + + requestQueryStringParams.Add("entity_class", + UriUtil.EncodeUriComponent(parameters.EntityClass, + OperationType, false, false, false)); + + if (parameters.EntityClassVersion.HasValue) + { + requestQueryStringParams.Add("entity_class_version", + parameters.EntityClassVersion.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + requestQueryStringParams.Add("entity_class_level", + UriUtil.EncodeUriComponent(parameters.EntityClassLevel, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Cursor)) + { + requestQueryStringParams.Add("cursor", + UriUtil.EncodeUriComponent(parameters.Cursor, + OperationType, false, false, false)); + } + + if (parameters.Limit.HasValue) + { + requestQueryStringParams.Add("limit", + parameters.Limit.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.FilterFast)) + { + requestQueryStringParams.Add("filter_fast", + UriUtil.EncodeUriComponent(parameters.FilterFast, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Filter)) + { + requestQueryStringParams.Add("filter", + UriUtil.EncodeUriComponent(parameters.Filter, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Sort)) + { + requestQueryStringParams.Add("sort", + UriUtil.EncodeUriComponent(parameters.Sort, + OperationType, false, false, false)); + } + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments, + Query = requestQueryStringParams + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesParameters.cs new file mode 100644 index 000000000..4ba572956 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntitiesParameters.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for listing entities via DataSync. + /// + public class GetEntitiesParameters + { + /// + /// Entity class name to filter by. Required. + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Optional — if not provided the server + /// returns entities matching the latest version. + /// + public int? EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// Pagination cursor returned from a previous request. + /// + public string Cursor { get; set; } + + /// + /// Maximum number of items to return per page. + /// Min 1, max 100, default 20. + /// + public int? Limit { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against strongly + /// consistent storage. Supports a limited number of conditions. + /// Example: "status == 'active'". + /// + public string FilterFast { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against eventually + /// consistent storage. Supports logical operators and nested conditions. + /// + public string Filter { get; set; } + + /// + /// Comma-separated list of fields to sort by, each optionally suffixed with + /// :desc (ascending by default). Example: "createdAt:desc,id". + /// + public string Sort { get; set; } + } + + /// + /// Result returned by GetEntities (list) containing an array of entities + /// plus cursor-based pagination metadata. + /// + public class PNDataSyncEntitiesListResult + { + public List Data { get; internal set; } = new(); + public PaginationMeta Meta { get; internal set; } + } + + /// + /// Cursor-based pagination metadata returned in list responses. + /// DataSync pagination is forward-only. + /// + public class PaginationMeta + { + public string NextCursor { get; internal set; } + public bool HasNext { get; internal set; } + public int? Limit { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityOperation.cs new file mode 100644 index 000000000..1d3d52def --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityOperation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetEntityOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetEntityParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetEntity; + + public GetEntityOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetEntityParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetEntityAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetEntityAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Entity Id", + new ArgumentException("Missing Entity Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityParameters.cs new file mode 100644 index 000000000..be6c079f8 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/GetEntityParameters.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for retrieving an entity by ID via DataSync. + /// + public class GetEntityParameters + { + /// + /// Entity identifier. Required. + /// + public string Id { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncDeleteEntityResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncDeleteEntityResult.cs new file mode 100644 index 000000000..49705f0eb --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncDeleteEntityResult.cs @@ -0,0 +1,5 @@ +namespace PubnubApi.EndPoint; + +public class PNDataSyncDeleteEntityResult +{ +} \ No newline at end of file diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncEntityResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncEntityResult.cs new file mode 100644 index 000000000..824825670 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/PNDataSyncEntityResult.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint; + +/// +/// Result returned by a single-entity operation (Create, Get, Update, Patch). +/// The server response envelope is deserialized into this type. +/// +public class PNDataSyncEntityResult +{ + public string Id { get; internal set; } + public string EntityClass { get; internal set; } + public int EntityClassVersion { get; internal set; } + public string EntityClassLevel { get; internal set; } + public string Status { get; internal set; } + public Dictionary Payload { get; internal set; } + public string CreatedAt { get; internal set; } + public string UpdatedAt { get; internal set; } + public string ETag { get; internal set; } + public string ExpiresAt { get; internal set; } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityOperation.cs new file mode 100644 index 000000000..6b0d51801 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class SetEntityOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly SetEntityParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncUpdateEntity; + + public SetEntityOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + SetEntityParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await UpdateEntityAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> UpdateEntityAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Entity Id", + new ArgumentException("Missing Entity Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var putBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PUT, + PathSegment = pathSegments, + BodyContentString = putBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.entity+json;version=1"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityParameters.cs new file mode 100644 index 000000000..466851587 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/SetEntityParameters.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for updating an entity with complete resource replacement (PUT) via DataSync. + /// + public class SetEntityParameters + { + /// + /// Entity identifier. Required. + /// + public string Id { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// Note: entityClass is immutable and cannot be changed after creation. + /// + public int EntityClassVersion { get; set; } + + /// + /// Entity status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// Replaces the entire payload — omitted fields are removed. + /// + public Dictionary Payload { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the update when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityOperation.cs new file mode 100644 index 000000000..23da750f3 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityOperation.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class UpdateEntityOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly UpdateEntityParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncPatchEntity; + + private static readonly HashSet OpsRequiringValue = + new () { JsonPatchOperationType.Add, JsonPatchOperationType.Replace, JsonPatchOperationType.Test }; + + private static readonly HashSet OpsRequiringFrom = + new () { JsonPatchOperationType.Move, JsonPatchOperationType.Copy }; + + public UpdateEntityOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + UpdateEntityParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await PatchEntityAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> PatchEntityAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Entity Id", + new ArgumentException("Missing Entity Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.Operations == null || parameters.Operations.Count == 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Operations list must contain at least one operation", + new ArgumentException("Operations list must contain at least one operation")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var patchArray = new List>(); + + foreach (var op in parameters.Operations) + { + var entry = new Dictionary + { + { "op", op.Op.ToString().ToLowerInvariant() }, + { "path", op.Path } + }; + + if (OpsRequiringValue.Contains(op.Op)) + { + entry["value"] = op.Value; + } + + if (OpsRequiringFrom.Contains(op.Op) && !string.IsNullOrEmpty(op.From)) + { + entry["from"] = op.From; + } + + patchArray.Add(entry); + } + + var patchBody = jsonLibrary.SerializeToJsonString(patchArray); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "entities", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PATCH, + PathSegment = pathSegments, + BodyContentString = patchBody + }; + + requestParameter.Headers.Add("Content-Type", "application/json-patch+json"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityParameters.cs new file mode 100644 index 000000000..ea1a5f597 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Entity/UpdateEntityParameters.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class UpdateEntityParameters + { + /// + /// Entity identifier. Required. + /// + public string Id { get; set; } + + /// + /// List of JSON Patch operations (RFC 6902) to apply. Required; must contain at least one operation. + /// + public List Operations { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the patch when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } + + public enum JsonPatchOperationType + { + Add, + Remove, + Replace, + Move, + Copy, + Test + } + + /// + /// A single JSON Patch operation as defined by RFC 6902. + /// + public class JsonPatchOperation + { + /// + /// The operation to perform: "add", "remove", "replace", "move", "copy", or "test". + /// + public JsonPatchOperationType Op { get; set; } + + /// + /// JSON Pointer (RFC 6901) to the target location. + /// + public string Path { get; set; } + + /// + /// The value to apply. Required for "add", "replace", and "test" operations. + /// Can be any JSON-serializable value including null. + /// + public object Value { get; set; } + + /// + /// Source location (JSON Pointer). Required for "move" and "copy" operations. + /// + public string From { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipOperation.cs new file mode 100644 index 000000000..3f3012830 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipOperation.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class CreateMembershipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly CreateMembershipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncCreateMembership; + + public CreateMembershipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + CreateMembershipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await CreateMembershipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> CreateMembershipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.ChannelId) || string.IsNullOrEmpty(parameters.ChannelId.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing ChannelId", + new ArgumentException("Missing ChannelId")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(parameters.UserId) || string.IsNullOrEmpty(parameters.UserId.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing UserId", + new ArgumentException("Missing UserId")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.MembershipClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("MembershipClassVersion must be >= 1", + new ArgumentException("MembershipClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = true + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == 201) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, 201, + null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.Id)) + { + dataProperties.Add("id", parameters.Id); + } + + dataProperties.Add("channelId", parameters.ChannelId); + dataProperties.Add("userId", parameters.UserId); + dataProperties.Add("relationshipClassVersion", parameters.MembershipClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var postBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships" + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.POST, + PathSegment = pathSegments, + BodyContentString = postBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.membership+json;version=1"); + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipParameters.cs new file mode 100644 index 000000000..78e9a0280 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/CreateMembershipParameters.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class CreateMembershipParameters + { + /// + /// Membership identifier. Optional — if not provided, the server generates one. + /// Must be 1–255 characters if provided. + /// + public string Id { get; set; } + + /// + /// Channel ID. Required. Immutable after creation. + /// Maps to entityAId on the wire. + /// + public string ChannelId { get; set; } + + /// + /// User ID. Required. Immutable after creation. + /// Maps to entityBId on the wire. + /// + public string UserId { get; set; } + + /// + /// Version of the membership relationship class. Required. Must be >= 1. + /// + public int MembershipClassVersion { get; set; } + + /// + /// Membership status (e.g., "active", "banned"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// + public Dictionary Payload { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipOperation.cs new file mode 100644 index 000000000..e0ea86c17 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipOperation.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class DeleteMembershipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly DeleteMembershipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncDeleteMembership; + + public DeleteMembershipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + DeleteMembershipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await DeleteMembershipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> DeleteMembershipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Membership Id", + new ArgumentException("Missing Membership Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + logger?.Trace($"{GetType().Name} request finished with status code {transportResponse.StatusCode}"); + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + returnValue.Result = new PNDataSyncDeleteMembershipResult(); + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.DELETE, + PathSegment = pathSegments + }; + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipParameters.cs new file mode 100644 index 000000000..b9f26d685 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/DeleteMembershipParameters.cs @@ -0,0 +1,16 @@ +namespace PubnubApi.EndPoint +{ + public class DeleteMembershipParameters + { + /// + /// Membership identifier. Required. + /// + public string Id { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the delete when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipOperation.cs new file mode 100644 index 000000000..e940faad1 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipOperation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetMembershipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetMembershipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetMembership; + + public GetMembershipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetMembershipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetMembershipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetMembershipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Membership Id", + new ArgumentException("Missing Membership Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipParameters.cs new file mode 100644 index 000000000..46365381e --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipParameters.cs @@ -0,0 +1,10 @@ +namespace PubnubApi.EndPoint +{ + public class GetMembershipParameters + { + /// + /// Membership identifier. Required. + /// + public string Id { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsOperation.cs new file mode 100644 index 000000000..f6c0385a7 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsOperation.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetDataSyncMembershipsOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetMembershipsParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetMemberships; + + public GetDataSyncMembershipsOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetMembershipsParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetMembershipsAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetMembershipsAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships" + }; + + var requestQueryStringParams = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.UserId)) + { + requestQueryStringParams.Add("user_id", + UriUtil.EncodeUriComponent(parameters.UserId, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.ChannelId)) + { + requestQueryStringParams.Add("channel_id", + UriUtil.EncodeUriComponent(parameters.ChannelId, + OperationType, false, false, false)); + } + + if (parameters.RelationshipClassVersion.HasValue) + { + requestQueryStringParams.Add("relationship_class_version", + parameters.RelationshipClassVersion.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.Cursor)) + { + requestQueryStringParams.Add("cursor", + UriUtil.EncodeUriComponent(parameters.Cursor, + OperationType, false, false, false)); + } + + if (parameters.Limit.HasValue) + { + requestQueryStringParams.Add("limit", + parameters.Limit.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.FilterFast)) + { + requestQueryStringParams.Add("filter_fast", + UriUtil.EncodeUriComponent(parameters.FilterFast, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Filter)) + { + requestQueryStringParams.Add("filter", + UriUtil.EncodeUriComponent(parameters.Filter, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Sort)) + { + requestQueryStringParams.Add("sort", + UriUtil.EncodeUriComponent(parameters.Sort, + OperationType, false, false, false)); + } + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments, + Query = requestQueryStringParams + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsParameters.cs new file mode 100644 index 000000000..687f779a1 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/GetMembershipsParameters.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class GetMembershipsParameters + { + /// + /// Filter memberships by user ID. Optional. + /// + public string UserId { get; set; } + + /// + /// Filter memberships by channel ID. Optional. + /// + public string ChannelId { get; set; } + + /// + /// Version of the relationship class. Optional — if not provided the server + /// returns memberships matching the latest version. + /// + public int? RelationshipClassVersion { get; set; } + + /// + /// Pagination cursor returned from a previous request. + /// + public string Cursor { get; set; } + + /// + /// Maximum number of items to return per page. + /// Min 1, max 100, default 20. + /// + public int? Limit { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against strongly + /// consistent storage. Supports a limited number of conditions. + /// Example: "status == 'active'". + /// + public string FilterFast { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against eventually + /// consistent storage. Supports logical operators and nested conditions. + /// + public string Filter { get; set; } + + /// + /// Comma-separated list of fields to sort by, each optionally suffixed with + /// :desc (ascending by default). Example: "createdAt:desc,id". + /// + public string Sort { get; set; } + } + + public class PNDataSyncMembershipsListResult + { + public List Data { get; internal set; } = new(); + public PaginationMeta Meta { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncDeleteMembershipResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncDeleteMembershipResult.cs new file mode 100644 index 000000000..9d7896dce --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncDeleteMembershipResult.cs @@ -0,0 +1,5 @@ +namespace PubnubApi.EndPoint; + +public class PNDataSyncDeleteMembershipResult +{ +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncMembershipResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncMembershipResult.cs new file mode 100644 index 000000000..6c584a2b4 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/PNDataSyncMembershipResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint; + +public class PNDataSyncMembershipResult +{ + public string Id { get; internal set; } + public string ChannelId { get; internal set; } + public string UserId { get; internal set; } + public string RelationshipClass { get; internal set; } + public int RelationshipClassVersion { get; internal set; } + public string Status { get; internal set; } + public Dictionary Payload { get; internal set; } + public string CreatedAt { get; internal set; } + public string UpdatedAt { get; internal set; } + public string ETag { get; internal set; } + public string ExpiresAt { get; internal set; } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipOperation.cs new file mode 100644 index 000000000..628b77d8f --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class SetMembershipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly UpdateMembershipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncUpdateMembership; + + public SetMembershipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + UpdateMembershipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await UpdateMembershipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> UpdateMembershipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Membership Id", + new ArgumentException("Missing Membership Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.RelationshipClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("RelationshipClassVersion must be >= 1", + new ArgumentException("RelationshipClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + dataProperties.Add("relationshipClassVersion", parameters.RelationshipClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var putBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PUT, + PathSegment = pathSegments, + BodyContentString = putBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.membership+json;version=1"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipParameters.cs new file mode 100644 index 000000000..3763735dc --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/SetMembershipParameters.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class UpdateMembershipParameters + { + /// + /// Membership identifier. Required. + /// + public string Id { get; set; } + + /// + /// Version of the membership relationship class. Required. Must be >= 1. + /// + public int RelationshipClassVersion { get; set; } + + /// + /// Membership status (e.g., "active", "banned"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// Replaces the entire payload — omitted fields are removed. + /// + public Dictionary Payload { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the update when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipOperation.cs new file mode 100644 index 000000000..2b05995ed --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipOperation.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class UpdateMembershipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly PatchMembershipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncPatchMembership; + + private static readonly HashSet OpsRequiringValue = + new () { JsonPatchOperationType.Add, JsonPatchOperationType.Replace, JsonPatchOperationType.Test }; + + private static readonly HashSet OpsRequiringFrom = + new () { JsonPatchOperationType.Move, JsonPatchOperationType.Copy }; + + public UpdateMembershipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + PatchMembershipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await PatchMembershipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> PatchMembershipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Membership Id", + new ArgumentException("Missing Membership Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.Operations == null || parameters.Operations.Count == 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Operations list must contain at least one operation", + new ArgumentException("Operations list must contain at least one operation")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var patchArray = new List>(); + + foreach (var op in parameters.Operations) + { + var entry = new Dictionary + { + { "op", op.Op.ToString().ToLowerInvariant() }, + { "path", op.Path } + }; + + if (OpsRequiringValue.Contains(op.Op)) + { + entry["value"] = op.Value; + } + + if (OpsRequiringFrom.Contains(op.Op) && !string.IsNullOrEmpty(op.From)) + { + entry["from"] = op.From; + } + + patchArray.Add(entry); + } + + var patchBody = jsonLibrary.SerializeToJsonString(patchArray); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "memberships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PATCH, + PathSegment = pathSegments, + BodyContentString = patchBody + }; + + requestParameter.Headers.Add("Content-Type", "application/json-patch+json"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipParameters.cs new file mode 100644 index 000000000..16e45831d --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Membership/UpdateMembershipParameters.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class PatchMembershipParameters + { + /// + /// Membership identifier. Required. + /// + public string Id { get; set; } + + /// + /// List of JSON Patch operations (RFC 6902) to apply. + /// + public List Operations { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the patch when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipOperation.cs new file mode 100644 index 000000000..fd26c9687 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipOperation.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class CreateRelationshipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly CreateRelationshipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncCreateRelationship; + + public CreateRelationshipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + CreateRelationshipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await CreateRelationshipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> CreateRelationshipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.EntityAId) || string.IsNullOrEmpty(parameters.EntityAId.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing EntityAId", + new ArgumentException("Missing EntityAId")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(parameters.EntityBId) || string.IsNullOrEmpty(parameters.EntityBId.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing EntityBId", + new ArgumentException("Missing EntityBId")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(parameters.RelationshipClass) || string.IsNullOrEmpty(parameters.RelationshipClass.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing RelationshipClass", + new ArgumentException("Missing RelationshipClass")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.RelationshipClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("RelationshipClassVersion must be >= 1", + new ArgumentException("RelationshipClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = true + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == 201) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, 201, + null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.Id)) + { + dataProperties.Add("id", parameters.Id); + } + + dataProperties.Add("entityAId", parameters.EntityAId); + dataProperties.Add("entityBId", parameters.EntityBId); + dataProperties.Add("relationshipClass", parameters.RelationshipClass); + dataProperties.Add("relationshipClassVersion", parameters.RelationshipClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var postBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships" + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.POST, + PathSegment = pathSegments, + BodyContentString = postBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.relationship+json;version=1"); + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipParameters.cs new file mode 100644 index 000000000..03afac7ef --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/CreateRelationshipParameters.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for creating a new relationship via DataSync. + /// + public class CreateRelationshipParameters + { + /// + /// Relationship identifier. Optional — if not provided, the server generates one. + /// Must be 1–255 characters if provided. + /// + public string Id { get; set; } + + /// + /// First entity ID (the "A" side of the relationship). Required. Immutable after creation. + /// Must be 1–255 characters. + /// + public string EntityAId { get; set; } + + /// + /// Second entity ID (the "B" side of the relationship). Required. Immutable after creation. + /// Must be 1–255 characters. + /// + public string EntityBId { get; set; } + + /// + /// Relationship class identifier (e.g., "ProductOwner", "FriendOf"). + /// Required. Immutable after creation. + /// + public string RelationshipClass { get; set; } + + /// + /// Version of the relationship class. Required. Must be >= 1. + /// + public int RelationshipClassVersion { get; set; } + + /// + /// Relationship status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// + public Dictionary Payload { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipOperation.cs new file mode 100644 index 000000000..e11d1ac67 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipOperation.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class DeleteRelationshipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly DeleteRelationshipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncDeleteRelationship; + + public DeleteRelationshipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + DeleteRelationshipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await DeleteRelationshipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> DeleteRelationshipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Relationship Id", + new ArgumentException("Missing Relationship Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + logger?.Trace($"{GetType().Name} request finished with status code {transportResponse.StatusCode}"); + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + returnValue.Result = new PNDataSyncDeleteRelationshipResult(); + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.DELETE, + PathSegment = pathSegments + }; + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipParameters.cs new file mode 100644 index 000000000..55dcbba8e --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/DeleteRelationshipParameters.cs @@ -0,0 +1,19 @@ +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for deleting a relationship by ID via DataSync. + /// + public class DeleteRelationshipParameters + { + /// + /// Relationship identifier. Required. + /// + public string Id { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the delete when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipOperation.cs new file mode 100644 index 000000000..13dd35fea --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipOperation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetRelationshipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetRelationshipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetRelationship; + + public GetRelationshipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetRelationshipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetRelationshipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetRelationshipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Relationship Id", + new ArgumentException("Missing Relationship Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipParameters.cs new file mode 100644 index 000000000..51d859de1 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipParameters.cs @@ -0,0 +1,13 @@ +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for retrieving a relationship by ID via DataSync. + /// + public class GetRelationshipParameters + { + /// + /// Relationship identifier. Required. + /// + public string Id { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsOperation.cs new file mode 100644 index 000000000..644873f4f --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsOperation.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetRelationshipsOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetRelationshipsParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetRelationships; + + public GetRelationshipsOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetRelationshipsParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetRelationshipsAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetRelationshipsAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.RelationshipClass) || string.IsNullOrEmpty(parameters.RelationshipClass.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing RelationshipClass", + new ArgumentException("Missing RelationshipClass")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.RelationshipClassVersion.HasValue && parameters.RelationshipClassVersion.Value < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("RelationshipClassVersion must be >= 1", + new ArgumentException("RelationshipClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships" + }; + + var requestQueryStringParams = new Dictionary(); + + requestQueryStringParams.Add("relationship_class", + UriUtil.EncodeUriComponent(parameters.RelationshipClass, + OperationType, false, false, false)); + + if (!string.IsNullOrEmpty(parameters.EntityAId)) + { + requestQueryStringParams.Add("entity_a_id", + UriUtil.EncodeUriComponent(parameters.EntityAId, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.EntityBId)) + { + requestQueryStringParams.Add("entity_b_id", + UriUtil.EncodeUriComponent(parameters.EntityBId, + OperationType, false, false, false)); + } + + if (parameters.RelationshipClassVersion.HasValue) + { + requestQueryStringParams.Add("relationship_class_version", + parameters.RelationshipClassVersion.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.Cursor)) + { + requestQueryStringParams.Add("cursor", + UriUtil.EncodeUriComponent(parameters.Cursor, + OperationType, false, false, false)); + } + + if (parameters.Limit.HasValue) + { + requestQueryStringParams.Add("limit", + parameters.Limit.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.FilterFast)) + { + requestQueryStringParams.Add("filter_fast", + UriUtil.EncodeUriComponent(parameters.FilterFast, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Filter)) + { + requestQueryStringParams.Add("filter", + UriUtil.EncodeUriComponent(parameters.Filter, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Sort)) + { + requestQueryStringParams.Add("sort", + UriUtil.EncodeUriComponent(parameters.Sort, + OperationType, false, false, false)); + } + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments, + Query = requestQueryStringParams + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsParameters.cs new file mode 100644 index 000000000..d5e6b17ce --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/GetRelationshipsParameters.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for listing relationships via DataSync. + /// + public class GetRelationshipsParameters + { + /// + /// Relationship class name to filter by. Required. + /// + public string RelationshipClass { get; set; } + + /// + /// Filter relationships by the first entity (A-side) ID. Optional. + /// At least one of EntityAId or EntityBId should be provided for meaningful results. + /// + public string EntityAId { get; set; } + + /// + /// Filter relationships by the second entity (B-side) ID. Optional. + /// At least one of EntityAId or EntityBId should be provided for meaningful results. + /// + public string EntityBId { get; set; } + + /// + /// Version of the relationship class. Optional — if not provided the server + /// uses the latest version. Must be >= 1 when provided. + /// + public int? RelationshipClassVersion { get; set; } + + /// + /// Pagination cursor returned from a previous request. + /// + public string Cursor { get; set; } + + /// + /// Maximum number of items to return per page. + /// Min 1, max 100, default 20. + /// + public int? Limit { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against strongly + /// consistent storage. Supports a limited number of conditions. + /// Example: "status == 'active'". + /// + public string FilterFast { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against eventually + /// consistent storage. Supports logical operators and nested conditions. + /// + public string Filter { get; set; } + + /// + /// Comma-separated list of fields to sort by, each optionally suffixed with + /// :desc (ascending by default). Example: "createdAt:desc,id". + /// + public string Sort { get; set; } + } + + /// + /// Result returned by GetRelationships (list) containing an array of relationships + /// plus cursor-based pagination metadata. + /// + public class PNDataSyncRelationshipsListResult + { + public List Data { get; internal set; } = new(); + public PaginationMeta Meta { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncDeleteRelationshipResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncDeleteRelationshipResult.cs new file mode 100644 index 000000000..7ba18de2f --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncDeleteRelationshipResult.cs @@ -0,0 +1,5 @@ +namespace PubnubApi.EndPoint; + +public class PNDataSyncDeleteRelationshipResult +{ +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncRelationshipResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncRelationshipResult.cs new file mode 100644 index 000000000..39a464999 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/PNDataSyncRelationshipResult.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint; + +/// +/// Result returned by a single-relationship operation (Create, Get, Update, Patch). +/// The server response envelope is deserialized into this type. +/// +public class PNDataSyncRelationshipResult +{ + public string Id { get; internal set; } + public string EntityAId { get; internal set; } + public string EntityBId { get; internal set; } + public string RelationshipClass { get; internal set; } + public int RelationshipClassVersion { get; internal set; } + public string Status { get; internal set; } + public Dictionary Payload { get; internal set; } + public string CreatedAt { get; internal set; } + public string UpdatedAt { get; internal set; } + public string ETag { get; internal set; } + public string ExpiresAt { get; internal set; } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipOperation.cs new file mode 100644 index 000000000..dc51d1d73 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class SetRelationshipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly SetRelationshipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncUpdateRelationship; + + public SetRelationshipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + SetRelationshipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await UpdateRelationshipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> UpdateRelationshipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Relationship Id", + new ArgumentException("Missing Relationship Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.RelationshipClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("RelationshipClassVersion must be >= 1", + new ArgumentException("RelationshipClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + dataProperties.Add("relationshipClassVersion", parameters.RelationshipClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var putBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PUT, + PathSegment = pathSegments, + BodyContentString = putBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.relationship+json;version=1"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipParameters.cs new file mode 100644 index 000000000..dfa637da6 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/SetRelationshipParameters.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for updating a relationship with complete resource replacement (PUT) via DataSync. + /// + public class SetRelationshipParameters + { + /// + /// Relationship identifier. Required. + /// + public string Id { get; set; } + + /// + /// Version of the relationship class. Required. Must be >= 1. + /// Note: entityAId, entityBId, and relationshipClass are immutable + /// and cannot be changed after creation. + /// + public int RelationshipClassVersion { get; set; } + + /// + /// Relationship status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// Replaces the entire payload — omitted fields are removed. + /// + public Dictionary Payload { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the update when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipOperation.cs new file mode 100644 index 000000000..91a7dbef5 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipOperation.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class UpdateRelationshipOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly UpdateRelationshipParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncPatchRelationship; + + private static readonly HashSet OpsRequiringValue = + new () { JsonPatchOperationType.Add, JsonPatchOperationType.Replace, JsonPatchOperationType.Test }; + + private static readonly HashSet OpsRequiringFrom = + new () { JsonPatchOperationType.Move, JsonPatchOperationType.Copy }; + + public UpdateRelationshipOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + UpdateRelationshipParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await PatchRelationshipAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> PatchRelationshipAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing Relationship Id", + new ArgumentException("Missing Relationship Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.Operations == null || parameters.Operations.Count == 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Operations list must contain at least one operation", + new ArgumentException("Operations list must contain at least one operation")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var patchArray = new List>(); + + foreach (var op in parameters.Operations) + { + var entry = new Dictionary + { + { "op", op.Op.ToString().ToLowerInvariant() }, + { "path", op.Path } + }; + + if (OpsRequiringValue.Contains(op.Op)) + { + entry["value"] = op.Value; + } + + if (OpsRequiringFrom.Contains(op.Op) && !string.IsNullOrEmpty(op.From)) + { + entry["from"] = op.From; + } + + patchArray.Add(entry); + } + + var patchBody = jsonLibrary.SerializeToJsonString(patchArray); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "relationships", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PATCH, + PathSegment = pathSegments, + BodyContentString = patchBody + }; + + requestParameter.Headers.Add("Content-Type", "application/json-patch+json"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipParameters.cs new file mode 100644 index 000000000..3013ac337 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/Relationship/UpdateRelationshipParameters.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + /// + /// Parameters for partially updating a relationship via JSON Patch (RFC 6902). + /// + public class UpdateRelationshipParameters + { + /// + /// Relationship identifier. Required. + /// + public string Id { get; set; } + + /// + /// List of JSON Patch operations (RFC 6902) to apply. Required; must contain at least one operation. + /// + public List Operations { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the patch when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserOperation.cs new file mode 100644 index 000000000..5812b6b82 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserOperation.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class CreateUserOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly CreateUserParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncCreateUser; + + public CreateUserOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + CreateUserParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await CreateUserAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> CreateUserAsync() + { + var returnValue = new PNResult(); + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = true + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == 201) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, 201, + null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.Id)) + { + dataProperties.Add("id", parameters.Id); + } + + if (!string.IsNullOrEmpty(parameters.EntityClass)) + { + dataProperties.Add("entityClass", parameters.EntityClass); + } + + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + dataProperties.Add("entityClassLevel", parameters.EntityClassLevel); + } + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var postBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users" + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.POST, + PathSegment = pathSegments, + BodyContentString = postBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.user+json;version=1"); + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserParameters.cs new file mode 100644 index 000000000..f7270bf9b --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/CreateUserParameters.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class CreateUserParameters + { + /// + /// User identifier. Optional — if not provided, the server generates one. + /// Must be 1–255 characters if provided. + /// + public string Id { get; set; } + + /// + /// Entity class name. Optional — defaults to "User" on the server if not + /// provided. Must be "User" or a subclass of "User". + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// + public int EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// User status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// + public Dictionary Payload { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserOperation.cs new file mode 100644 index 000000000..f89ba2fe1 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserOperation.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class DeleteUserOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly DeleteUserParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncDeleteUser; + + public DeleteUserOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + DeleteUserParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await DeleteUserAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> DeleteUserAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing User Id", + new ArgumentException("Missing User Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + logger?.Trace($"{GetType().Name} request finished with status code {transportResponse.StatusCode}"); + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + returnValue.Result = new PNDataSyncDeleteUserResult(); + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.DELETE, + PathSegment = pathSegments + }; + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserParameters.cs new file mode 100644 index 000000000..3b3633fa8 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/DeleteUserParameters.cs @@ -0,0 +1,16 @@ +namespace PubnubApi.EndPoint +{ + public class DeleteUserParameters + { + /// + /// User identifier. Required. + /// + public string Id { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the delete when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserOperation.cs new file mode 100644 index 000000000..6f63542d0 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserOperation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetUserOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetUserParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetUser; + + public GetUserOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetUserParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetUserAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetUserAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing User Id", + new ArgumentException("Missing User Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserParameters.cs new file mode 100644 index 000000000..f4522bf3f --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUserParameters.cs @@ -0,0 +1,10 @@ +namespace PubnubApi.EndPoint +{ + public class GetUserParameters + { + /// + /// User identifier. Required. + /// + public string Id { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersOperation.cs new file mode 100644 index 000000000..cbe749a8c --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersOperation.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class GetUsersOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly GetUsersParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncGetUsers; + + public GetUsersOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + GetUsersParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await GetUsersAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> GetUsersAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + Tuple JsonAndStatusTuple; + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users" + }; + + var requestQueryStringParams = new Dictionary(); + + if (!string.IsNullOrEmpty(parameters.EntityClass)) + { + requestQueryStringParams.Add("entity_class", + UriUtil.EncodeUriComponent(parameters.EntityClass, + OperationType, false, false, false)); + } + + if (parameters.EntityClassVersion.HasValue) + { + requestQueryStringParams.Add("entity_class_version", + parameters.EntityClassVersion.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.EntityClassLevel)) + { + requestQueryStringParams.Add("entity_class_level", + UriUtil.EncodeUriComponent(parameters.EntityClassLevel, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Cursor)) + { + requestQueryStringParams.Add("cursor", + UriUtil.EncodeUriComponent(parameters.Cursor, + OperationType, false, false, false)); + } + + if (parameters.Limit.HasValue) + { + requestQueryStringParams.Add("limit", + parameters.Limit.Value.ToString()); + } + + if (!string.IsNullOrEmpty(parameters.FilterFast)) + { + requestQueryStringParams.Add("filter_fast", + UriUtil.EncodeUriComponent(parameters.FilterFast, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Filter)) + { + requestQueryStringParams.Add("filter", + UriUtil.EncodeUriComponent(parameters.Filter, + OperationType, false, false, false)); + } + + if (!string.IsNullOrEmpty(parameters.Sort)) + { + requestQueryStringParams.Add("sort", + UriUtil.EncodeUriComponent(parameters.Sort, + OperationType, false, false, false)); + } + + var requestParameter = new RequestParameter + { + RequestType = Constants.GET, + PathSegment = pathSegments, + Query = requestQueryStringParams + }; + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersParameters.cs new file mode 100644 index 000000000..97f9b517b --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/GetUsersParameters.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class GetUsersParameters + { + /// + /// Entity class name to filter by. Optional — if not provided the server returns + /// users of every class descending from "User". + /// + public string EntityClass { get; set; } + + /// + /// Schema version of the entity class. Optional — if not provided the server + /// returns users matching the latest version. + /// + public int? EntityClassVersion { get; set; } + + /// + /// Class hierarchy level ("Global" or "SubKey") used to disambiguate classes + /// with the same name defined at different levels. Optional. + /// + public string EntityClassLevel { get; set; } + + /// + /// Pagination cursor returned from a previous request. + /// + public string Cursor { get; set; } + + /// + /// Maximum number of items to return per page. + /// Min 1, max 100, default 20. + /// + public int? Limit { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against strongly + /// consistent storage. Supports a limited number of conditions. + /// Example: "status == 'active'". + /// + public string FilterFast { get; set; } + + /// + /// Filter expression using AppContext Query Language, evaluated against eventually + /// consistent storage. Supports logical operators and nested conditions. + /// + public string Filter { get; set; } + + /// + /// Comma-separated list of fields to sort by, each optionally suffixed with + /// :desc (ascending by default). Example: "createdAt:desc,id". + /// + public string Sort { get; set; } + } + + public class PNDataSyncUsersListResult + { + public List Data { get; internal set; } = new(); + public PaginationMeta Meta { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncDeleteUserResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncDeleteUserResult.cs new file mode 100644 index 000000000..717226c98 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncDeleteUserResult.cs @@ -0,0 +1,5 @@ +namespace PubnubApi.EndPoint; + +public class PNDataSyncDeleteUserResult +{ +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncUserResult.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncUserResult.cs new file mode 100644 index 000000000..9bcdd191b --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/PNDataSyncUserResult.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint; + +public class PNDataSyncUserResult +{ + public string Id { get; internal set; } + public string EntityClass { get; internal set; } + public int EntityClassVersion { get; internal set; } + public string EntityClassLevel { get; internal set; } + public string Status { get; internal set; } + public Dictionary Payload { get; internal set; } + public string CreatedAt { get; internal set; } + public string UpdatedAt { get; internal set; } + public string ETag { get; internal set; } + public string ExpiresAt { get; internal set; } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserOperation.cs new file mode 100644 index 000000000..01eaf2e44 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class SetUserOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly SetUserParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncUpdateUser; + + public SetUserOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + SetUserParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await UpdateUserAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> UpdateUserAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing User Id", + new ArgumentException("Missing User Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.EntityClassVersion < 1) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("EntityClassVersion must be >= 1", + new ArgumentException("EntityClassVersion must be >= 1")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var dataProperties = new Dictionary(); + + dataProperties.Add("entityClassVersion", parameters.EntityClassVersion); + + if (!string.IsNullOrEmpty(parameters.Status)) + { + dataProperties.Add("status", parameters.Status); + } + + if (parameters.Payload != null) + { + dataProperties.Add("payload", parameters.Payload); + } + + var requestEnvelope = new Dictionary + { + { "data", dataProperties } + }; + + var putBody = jsonLibrary.SerializeToJsonString(requestEnvelope); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PUT, + PathSegment = pathSegments, + BodyContentString = putBody + }; + + requestParameter.Headers.Add("Content-Type", + "application/vnd.pubnub.objects.user+json;version=1"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserParameters.cs new file mode 100644 index 000000000..758eebc90 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/SetUserParameters.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class SetUserParameters + { + /// + /// User identifier. Required. + /// + public string Id { get; set; } + + /// + /// Schema version of the entity class. Required. Must be >= 1. + /// + public int EntityClassVersion { get; set; } + + /// + /// User status (e.g., "active", "inactive"). 1–100 characters. + /// + public string Status { get; set; } + + /// + /// User-defined custom properties. Supports arbitrarily nested objects. + /// Replaces the entire payload — omitted fields are removed. + /// + public Dictionary Payload { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the update when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserOperation.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserOperation.cs new file mode 100644 index 000000000..b03645789 --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserOperation.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PubnubApi.EndPoint +{ + public class UpdateUserOperation : PubnubCoreBase + { + private readonly PNConfiguration config; + private readonly IJsonPluggableLibrary jsonLibrary; + private readonly IPubnubUnitTest unit; + private readonly UpdateUserParameters parameters; + + private PNCallback savedCallback; + + private const PNOperationType OperationType = PNOperationType.PNDataSyncPatchUser; + + private static readonly HashSet OpsRequiringValue = + new () { JsonPatchOperationType.Add, JsonPatchOperationType.Replace, JsonPatchOperationType.Test }; + + private static readonly HashSet OpsRequiringFrom = + new () { JsonPatchOperationType.Move, JsonPatchOperationType.Copy }; + + public UpdateUserOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, + IPubnubUnitTest pubnubUnit, TokenManager tokenManager, Pubnub instance, + UpdateUserParameters parameters) : base(pubnubConfig, + jsonPluggableLibrary, pubnubUnit, tokenManager, instance) + { + config = pubnubConfig; + jsonLibrary = jsonPluggableLibrary; + unit = pubnubUnit; + this.parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + + public void Execute(PNCallback callback) + { + if (callback == null) + { + throw new ArgumentException("Missing userCallback"); + } + + savedCallback = callback; + ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + var status = new PNStatus + { + Error = true, + ErrorData = new PNErrorData(t.Exception?.Message, t.Exception) + }; + callback.OnResponse(default, status); + } + else + { + var pnResult = t.Result; + callback.OnResponse(pnResult.Result, pnResult.Status); + } + }); + } + + public async Task> ExecuteAsync() + { + logger?.Trace($"{GetType().Name} ExecuteAsync invoked."); + return await PatchUserAsync().ConfigureAwait(false); + } + + internal void Retry() + { + if (savedCallback != null) + { + Execute(savedCallback); + } + } + + private async Task> PatchUserAsync() + { + var returnValue = new PNResult(); + + if (string.IsNullOrEmpty(parameters.Id) || string.IsNullOrEmpty(parameters.Id.Trim())) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Missing User Id", + new ArgumentException("Missing User Id")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (parameters.Operations == null || parameters.Operations.Count == 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Operations list must contain at least one operation", + new ArgumentException("Operations list must contain at least one operation")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + if (string.IsNullOrEmpty(config.SubscribeKey) || string.IsNullOrEmpty(config.SubscribeKey.Trim()) || + config.SubscribeKey.Length <= 0) + { + var errStatus = new PNStatus + { + Error = true, + ErrorData = new PNErrorData("Invalid Subscribe key", + new ArgumentException("Invalid Subscribe key")) + }; + returnValue.Status = errStatus; + return returnValue; + } + + logger?.Trace($"{GetType().Name} parameter validated."); + var requestState = new RequestState + { + ResponseType = OperationType, + Reconnect = false, + EndPointOperation = this, + UsePostMethod = false + }; + + var requestParameter = CreateRequestParameter(); + Tuple JsonAndStatusTuple; + var transportRequest = PubnubInstance.transportMiddleware.PreapareTransportRequest( + requestParameter, OperationType); + var transportResponse = await PubnubInstance.transportMiddleware.Send(transportRequest) + .ConfigureAwait(false); + if (transportResponse.Error == null) + { + var responseString = Encoding.UTF8.GetString(transportResponse.Content); + var errorStatus = GetStatusIfError(requestState, responseString); + if (errorStatus == null && transportResponse.StatusCode == Constants.HttpRequestSuccessStatusCode) + { + requestState.GotJsonResponse = true; + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + requestState.ResponseType, PNStatusCategory.PNAcknowledgmentCategory, requestState, + Constants.HttpRequestSuccessStatusCode, null); + JsonAndStatusTuple = new Tuple(responseString, status); + } + else + { + JsonAndStatusTuple = new Tuple(string.Empty, errorStatus); + } + + returnValue.Status = JsonAndStatusTuple.Item2; + var json = JsonAndStatusTuple.Item1; + if (!string.IsNullOrEmpty(json)) + { + var resultList = ProcessJsonResponse(requestState, json); + var responseBuilder = new ResponseBuilder(config, jsonLibrary); + var responseResult = + responseBuilder.JsonToObject(resultList, true); + if (responseResult != null) + { + returnValue.Result = responseResult; + } + } + } + else + { + var statusCode = PNStatusCodeHelper.GetHttpStatusCode(transportResponse.Error.Message); + var category = + PNStatusCategoryHelper.GetPNStatusCategory(statusCode, transportResponse.Error.Message); + var status = new StatusBuilder(config, jsonLibrary).CreateStatusResponse( + OperationType, category, requestState, statusCode, + new PNException(transportResponse.Error.Message, transportResponse.Error)); + returnValue.Status = status; + } + + logger?.Trace($"{GetType().Name} request finished with status code {returnValue.Status.StatusCode}"); + return returnValue; + } + + private RequestParameter CreateRequestParameter() + { + var patchArray = new List>(); + + foreach (var op in parameters.Operations) + { + var entry = new Dictionary + { + { "op", op.Op.ToString().ToLowerInvariant() }, + { "path", op.Path } + }; + + if (OpsRequiringValue.Contains(op.Op)) + { + entry["value"] = op.Value; + } + + if (OpsRequiringFrom.Contains(op.Op) && !string.IsNullOrEmpty(op.From)) + { + entry["from"] = op.From; + } + + patchArray.Add(entry); + } + + var patchBody = jsonLibrary.SerializeToJsonString(patchArray); + + var pathSegments = new List + { + "v1", + "datasync", + "subkeys", + config.SubscribeKey, + "users", + parameters.Id + }; + + var requestParameter = new RequestParameter + { + RequestType = Constants.PATCH, + PathSegment = pathSegments, + BodyContentString = patchBody + }; + + requestParameter.Headers.Add("Content-Type", "application/json-patch+json"); + + if (!string.IsNullOrEmpty(parameters.IfMatch)) + { + requestParameter.Headers.Add("If-Match", parameters.IfMatch); + } + + return requestParameter; + } + } +} diff --git a/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserParameters.cs b/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserParameters.cs new file mode 100644 index 000000000..983912a5b --- /dev/null +++ b/src/Api/PubnubApi/EndPoint/DataSync/User/UpdateUserParameters.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace PubnubApi.EndPoint +{ + public class UpdateUserParameters + { + /// + /// User identifier. Required. + /// + public string Id { get; set; } + + /// + /// List of JSON Patch operations (RFC 6902) to apply. + /// + public List Operations { get; set; } + + /// + /// ETag for optimistic concurrency control. If provided, the server rejects + /// the patch when the current resource version does not match (HTTP 412). + /// + public string IfMatch { get; set; } + } +} diff --git a/src/Api/PubnubApi/EndPoint/Objects/SetChannelMetadataOperation.cs b/src/Api/PubnubApi/EndPoint/Objects/SetChannelMetadataOperation.cs index abc3989b3..128052484 100644 --- a/src/Api/PubnubApi/EndPoint/Objects/SetChannelMetadataOperation.cs +++ b/src/Api/PubnubApi/EndPoint/Objects/SetChannelMetadataOperation.cs @@ -381,7 +381,7 @@ private RequestParameter CreateRequestParameter() }; if (!string.IsNullOrEmpty(ifMatchesEtag)) { - requestParameter.Headers.Add("If-Match", ifMatchesEtag); + requestParameter.Headers.Add("If-Match", $"\"{ifMatchesEtag}\""); } return requestParameter; diff --git a/src/Api/PubnubApi/EndPoint/Objects/SetUuidMetadataOperation.cs b/src/Api/PubnubApi/EndPoint/Objects/SetUuidMetadataOperation.cs index 1cb17ec83..611cb7897 100644 --- a/src/Api/PubnubApi/EndPoint/Objects/SetUuidMetadataOperation.cs +++ b/src/Api/PubnubApi/EndPoint/Objects/SetUuidMetadataOperation.cs @@ -408,7 +408,7 @@ private RequestParameter CreateRequestParameter() }; if (!string.IsNullOrEmpty(ifMatchesEtag)) { - requestParameter.Headers.Add("If-Match", ifMatchesEtag); + requestParameter.Headers.Add("If-Match", $"\"{ifMatchesEtag}\""); } return requestParameter; diff --git a/src/Api/PubnubApi/EndPoint/TokenManager.cs b/src/Api/PubnubApi/EndPoint/TokenManager.cs index 3cc64933d..793d2db15 100644 --- a/src/Api/PubnubApi/EndPoint/TokenManager.cs +++ b/src/Api/PubnubApi/EndPoint/TokenManager.cs @@ -77,7 +77,8 @@ public PNTokenContent ParseToken(string token) ChannelGroups = new Dictionary(), Uuids = new Dictionary(), Users = new Dictionary(), - Spaces = new Dictionary() + Spaces = new Dictionary(), + DataSync = NewDataSyncScopes() }; result.Patterns = new PNTokenPatterns { @@ -85,7 +86,8 @@ public PNTokenContent ParseToken(string token) ChannelGroups = new Dictionary(), Uuids = new Dictionary(), Users = new Dictionary(), - Spaces = new Dictionary() + Spaces = new Dictionary(), + DataSync = NewDataSyncScopes() }; ParseCBOR(cborDict, ref result); } @@ -150,6 +152,24 @@ private void FillTokenPermissionMapping(Dictionary permissionVal permissionMapping.ChannelGroups.Add(resourceId, resourcePermission); } break; + case "datasync:entities": + if (!permissionMapping.DataSync.Entities.ContainsKey(resourceId)) + { + permissionMapping.DataSync.Entities.Add(resourceId, resourcePermission); + } + break; + case "datasync:relationships": + if (!permissionMapping.DataSync.Relationships.ContainsKey(resourceId)) + { + permissionMapping.DataSync.Relationships.Add(resourceId, resourcePermission); + } + break; + case "datasync:memberships": + if (!permissionMapping.DataSync.Memberships.ContainsKey(resourceId)) + { + permissionMapping.DataSync.Memberships.Add(resourceId, resourcePermission); + } + break; default: logger?.Error($"Unidentified resource ID when parsing permission mappings! {resourceId}"); break; @@ -176,6 +196,10 @@ private void ParseCBOR(Dictionary cbor, ref PNTokenContent pnGra break; case "meta": pnGrantTokenDecoded.Meta = (kvp.Value as Dictionary).ToDictionary(x => ByteToString(x.Key), y => y.Value); + if (pnGrantTokenDecoded.Meta.TryGetValue("pn-projections", out var projectionsValue)) + { + pnGrantTokenDecoded.Projections = DecodeProjections(projectionsValue as Dictionary); + } break; case "uuid": pnGrantTokenDecoded.AuthorizedUuid = ByteToString(kvp.Value); @@ -198,6 +222,107 @@ private void ParseCBOR(Dictionary cbor, ref PNTokenContent pnGra } } + private static PNDataSyncTokenScopes NewDataSyncScopes() + { + return new PNDataSyncTokenScopes + { + Entities = new Dictionary(), + Relationships = new Dictionary(), + Memberships = new Dictionary() + }; + } + + private PNDataSyncProjections DecodeProjections(Dictionary projections) + { + if (projections == null) + { + return null; + } + + var result = new PNDataSyncProjections(); + foreach (var kvp in projections) + { + var scopeKey = ByteToString(kvp.Key); + var scope = DecodeProjectionScope(kvp.Value as Dictionary); + switch (scopeKey) + { + case "res": + result.Resources = scope; + break; + case "pat": + result.Patterns = scope; + break; + default: + logger?.Error($"Unidentified projection scope key when parsing token! {scopeKey}"); + break; + } + } + return result; + } + + private PNDataSyncProjectionScope DecodeProjectionScope(Dictionary scopeValues) + { + var scope = new PNDataSyncProjectionScope + { + Entities = new Dictionary(), + Users = new Dictionary(), + Channels = new Dictionary(), + Relationships = new Dictionary(), + Memberships = new Dictionary() + }; + if (scopeValues == null) + { + return scope; + } + + foreach (var kvp in scopeValues) + { + // Flat composite key, e.g. "datasync:entities:user.A" -> projection name. + var compositeKey = ByteToString(kvp.Key); + var projectionName = ByteToString(kvp.Value); + + const string prefix = "datasync:"; + if (!compositeKey.StartsWith(prefix, StringComparison.Ordinal)) + { + logger?.Error($"Unidentified projection key when parsing token! {compositeKey}"); + continue; + } + + var remainder = compositeKey.Substring(prefix.Length); + var separatorIndex = remainder.IndexOf(':'); + if (separatorIndex <= 0 || separatorIndex >= remainder.Length - 1) + { + logger?.Error($"Malformed projection key when parsing token! {compositeKey}"); + continue; + } + + var type = remainder.Substring(0, separatorIndex); + var resourceId = remainder.Substring(separatorIndex + 1); + switch (type) + { + case "entities": + scope.Entities[resourceId] = projectionName; + break; + case "users": + scope.Users[resourceId] = projectionName; + break; + case "channels": + scope.Channels[resourceId] = projectionName; + break; + case "relationships": + scope.Relationships[resourceId] = projectionName; + break; + case "memberships": + scope.Memberships[resourceId] = projectionName; + break; + default: + logger?.Error($"Unidentified projection resource type when parsing token! {type}"); + break; + } + } + return scope; + } + public void SetAuthToken(string token) { dToken.AddOrUpdate(pubnubInstanceId, token, (k, o) => token); diff --git a/src/Api/PubnubApi/Enum/PNOperationType.cs b/src/Api/PubnubApi/Enum/PNOperationType.cs index 106289f6b..708534be2 100644 --- a/src/Api/PubnubApi/Enum/PNOperationType.cs +++ b/src/Api/PubnubApi/Enum/PNOperationType.cs @@ -76,6 +76,41 @@ public enum PNOperationType PNFileUrlOperation, PNDownloadFileOperation, PNListFilesOperation, - PNDeleteFileOperation + PNDeleteFileOperation, + + PNDataSyncCreateEntity, + PNDataSyncGetEntity, + PNDataSyncGetEntities, + PNDataSyncUpdateEntity, + PNDataSyncDeleteEntity, + PNDataSyncPatchEntity, + + PNDataSyncCreateRelationship, + PNDataSyncGetRelationship, + PNDataSyncGetRelationships, + PNDataSyncUpdateRelationship, + PNDataSyncDeleteRelationship, + PNDataSyncPatchRelationship, + + PNDataSyncCreateUser, + PNDataSyncGetUser, + PNDataSyncGetUsers, + PNDataSyncUpdateUser, + PNDataSyncDeleteUser, + PNDataSyncPatchUser, + + PNDataSyncCreateChannel, + PNDataSyncGetChannel, + PNDataSyncGetChannels, + PNDataSyncUpdateChannel, + PNDataSyncDeleteChannel, + PNDataSyncPatchChannel, + + PNDataSyncCreateMembership, + PNDataSyncGetMembership, + PNDataSyncGetMemberships, + PNDataSyncUpdateMembership, + PNDataSyncDeleteMembership, + PNDataSyncPatchMembership } } diff --git a/src/Api/PubnubApi/EventEngine/Common/EventEmitter.cs b/src/Api/PubnubApi/EventEngine/Common/EventEmitter.cs index 4866fbd3d..1985bd7f7 100644 --- a/src/Api/PubnubApi/EventEngine/Common/EventEmitter.cs +++ b/src/Api/PubnubApi/EventEngine/Common/EventEmitter.cs @@ -182,6 +182,23 @@ public void EmitEvent(object e) } } } + else if (eventData.MessageType == 5) //DataSync events + { + Dictionary dataSyncEventFields = payload as Dictionary ?? + (payload as JObject) + ?.ToObject>(); + Dictionary dataSyncMetadata = dataSyncEventFields != null + && dataSyncEventFields.ContainsKey("metadata") + && dataSyncEventFields["metadata"] != null + ? jsonLibrary.ConvertToDictionaryObject(dataSyncEventFields["metadata"]) + : null; + if (dataSyncMetadata != null + && dataSyncMetadata.TryGetValue("source", out var dataSyncSource) + && dataSyncSource?.ToString() == "data-sync") + { + jsonFields.Add("payload", payload); + } + } else { if ((configuration.CryptoModule != null || configuration.CipherKey.Length > 0) && @@ -441,6 +458,42 @@ public void EmitEvent(object e) break; } + case 5: + { + ResponseBuilder responseBuilder = new ResponseBuilder(configuration, jsonLibrary); + PNDataSyncEventResult dataSyncEvent = + responseBuilder.GetEventResultObject(jsonFields); + if (dataSyncEvent != null) + { + foreach (var listener in listeners) + { + SafeInvokeListener(() => listener?.DataSyncEvent(instance, dataSyncEvent), + "DataSyncEvent"); + } + + if (!string.IsNullOrEmpty(dataSyncEvent.Channel) && + channelListenersMap.ContainsKey(dataSyncEvent.Channel)) + { + foreach (var l in channelListenersMap[dataSyncEvent.Channel]) + { + SafeInvokeListener(() => l?.DataSyncEvent(instance, dataSyncEvent), + "DataSyncEvent"); + } + } + + if (!string.IsNullOrEmpty(dataSyncEvent.Subscription) && + channelGroupListenersMap.ContainsKey(dataSyncEvent.Subscription)) + { + foreach (var l in channelGroupListenersMap[dataSyncEvent.Subscription]) + { + SafeInvokeListener(() => l?.DataSyncEvent(instance, dataSyncEvent), + "DataSyncEvent"); + } + } + } + + break; + } default: { if (currentMessageChannel.Contains("-pnpres")) diff --git a/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs b/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs index 89325c287..df7b9e3de 100644 --- a/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs +++ b/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using PubnubApi.EndPoint; namespace PubnubApi { @@ -987,6 +988,96 @@ public static T DeserializeToInternalObject(IJsonPluggableLibrary jsonPlug, L #endregion } + else if (typeof(T) == typeof(PNDataSyncEntityResult)) + { + #region "PNCreateEntityResult" + + PNDataSyncEntityResult result = PNDataSyncEntityResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncEntityResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncEntitiesListResult)) + { + #region "PNCreateEntityResult" + + PNDataSyncEntitiesListResult result = PNDataSyncEntitiesListResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncEntitiesListResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncRelationshipResult)) + { + #region "PNDataSyncRelationshipResult" + + PNDataSyncRelationshipResult result = PNDataSyncRelationshipResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncRelationshipResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncRelationshipsListResult)) + { + #region "PNDataSyncRelationshipsListResult" + + PNDataSyncRelationshipsListResult result = PNDataSyncRelationshipsListResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncRelationshipsListResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncUserResult)) + { + #region "PNDataSyncUserResult" + + PNDataSyncUserResult result = PNDataSyncUserResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncUserResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncUsersListResult)) + { + #region "PNDataSyncUsersListResult" + + PNDataSyncUsersListResult result = PNDataSyncUsersListResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncUsersListResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncChannelResult)) + { + #region "PNDataSyncChannelResult" + + PNDataSyncChannelResult result = PNDataSyncChannelResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncChannelResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncChannelsListResult)) + { + #region "PNDataSyncChannelsListResult" + + PNDataSyncChannelsListResult result = PNDataSyncChannelsListResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncChannelsListResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncMembershipResult)) + { + #region "PNDataSyncMembershipResult" + + PNDataSyncMembershipResult result = PNDataSyncMembershipResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncMembershipResult), CultureInfo.InvariantCulture); + + #endregion + } + else if (typeof(T) == typeof(PNDataSyncMembershipsListResult)) + { + #region "PNDataSyncMembershipsListResult" + + PNDataSyncMembershipsListResult result = PNDataSyncMembershipsListResultJsonDataParse.GetObject(jsonPlug, listObject); + ret = (T)Convert.ChangeType(result, typeof(PNDataSyncMembershipsListResult), CultureInfo.InvariantCulture); + + #endregion + } else { System.Diagnostics.Debug.WriteLine("DeserializeToObject(list) => NO MATCH"); diff --git a/src/Api/PubnubApi/JsonDataParse/EventDeserializer.cs b/src/Api/PubnubApi/JsonDataParse/EventDeserializer.cs index aff8829cc..0e4e0b5a2 100644 --- a/src/Api/PubnubApi/JsonDataParse/EventDeserializer.cs +++ b/src/Api/PubnubApi/JsonDataParse/EventDeserializer.cs @@ -38,6 +38,11 @@ public T Deserialize(IDictionary json) PNMessageActionEventResult messageActionEvent = PNMessageActionEventJsonDataParse.GetObject(jsonLibrary, json); response = (T)Convert.ChangeType(messageActionEvent, typeof(PNMessageActionEventResult), CultureInfo.InvariantCulture); } + else if (typeof(T) == typeof(PNDataSyncEventResult)) + { + PNDataSyncEventResult dataSyncEvent = PNDataSyncEventJsonDataParse.GetObject(jsonLibrary, json); + response = (T)Convert.ChangeType(dataSyncEvent, typeof(PNDataSyncEventResult), CultureInfo.InvariantCulture); + } return response; } diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelResultJsonDataParse.cs new file mode 100644 index 000000000..2112ab9c7 --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelResultJsonDataParse.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncChannelResultJsonDataParse +{ + internal static PNDataSyncChannelResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + Dictionary dictionaryObject = (listObject != null && listObject.Count == 1) + ? jsonPlug.ConvertToDictionaryObject(listObject[0]) + : null; + PNDataSyncChannelResult result = null; + if (dictionaryObject != null && dictionaryObject.ContainsKey("data")) + { + result = new PNDataSyncChannelResult(); + + Dictionary dataDictionary = + jsonPlug.ConvertToDictionaryObject(dictionaryObject["data"]); + if (dataDictionary != null && dataDictionary.Count > 0) + { + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + result.EntityClass = dataDictionary.ContainsKey("entityClass") && dataDictionary["entityClass"] != null + ? dataDictionary["entityClass"].ToString() + : null; + result.EntityClassVersion = dataDictionary.ContainsKey("entityClassVersion") && dataDictionary["entityClassVersion"] != null + ? (int)(long)dataDictionary["entityClassVersion"] + : 1; + result.EntityClassLevel = dataDictionary.ContainsKey("entityClassLevel") && dataDictionary["entityClassLevel"] != null + ? dataDictionary["entityClassLevel"].ToString() + : null; + result.Status = dataDictionary.ContainsKey("status") && dataDictionary["status"] != null + ? dataDictionary["status"].ToString() + : null; + result.CreatedAt = dataDictionary.ContainsKey("createdAt") && dataDictionary["createdAt"] != null + ? dataDictionary["createdAt"].ToString() + : null; + result.UpdatedAt = dataDictionary.ContainsKey("updatedAt") && dataDictionary["updatedAt"] != null + ? dataDictionary["updatedAt"].ToString() + : null; + result.ETag = dataDictionary.ContainsKey("eTag") && dataDictionary["eTag"] != null + ? dataDictionary["eTag"].ToString() + : null; + result.ExpiresAt = dataDictionary.ContainsKey("expiresAt") && dataDictionary["expiresAt"] != null + ? dataDictionary["expiresAt"].ToString() + : null; + if (dataDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + result.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelsListResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelsListResultJsonDataParse.cs new file mode 100644 index 000000000..af6dc0789 --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncChannelsListResultJsonDataParse.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncChannelsListResultJsonDataParse +{ + internal static PNDataSyncChannelsListResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + var result = new PNDataSyncChannelsListResult(); + foreach (var rawObject in listObject) + { + var objectDictionary = jsonPlug.ConvertToDictionaryObject(rawObject); + if (objectDictionary == null || !objectDictionary.Any()) + { + continue; + } + + if (objectDictionary.TryGetValue("data", out var dataObject) && dataObject != null) + { + var dataArray = jsonPlug.ConvertToObjectArray(dataObject); + if (dataArray == null || !dataArray.Any()) + { + continue; + } + + for (int i = 0; i < dataArray.Length; i++) + { + if (dataArray[i] is not Dictionary dataEntryDictionary) + { + continue; + } + var data = new PNDataSyncChannelResult + { + Id = dataEntryDictionary.ContainsKey("id") && dataEntryDictionary["id"] != null + ? dataEntryDictionary["id"].ToString() + : null, + EntityClass = dataEntryDictionary.ContainsKey("entityClass") && dataEntryDictionary["entityClass"] != null + ? dataEntryDictionary["entityClass"].ToString() + : null, + EntityClassVersion = dataEntryDictionary.ContainsKey("entityClassVersion") && dataEntryDictionary["entityClassVersion"] != null + ? (int)(long)dataEntryDictionary["entityClassVersion"] + : 1, + EntityClassLevel = dataEntryDictionary.ContainsKey("entityClassLevel") && dataEntryDictionary["entityClassLevel"] != null + ? dataEntryDictionary["entityClassLevel"].ToString() + : null, + Status = dataEntryDictionary.ContainsKey("status") && dataEntryDictionary["status"] != null + ? dataEntryDictionary["status"].ToString() + : null, + CreatedAt = dataEntryDictionary.ContainsKey("createdAt") && dataEntryDictionary["createdAt"] != null + ? dataEntryDictionary["createdAt"].ToString() + : null, + UpdatedAt = dataEntryDictionary.ContainsKey("updatedAt") && dataEntryDictionary["updatedAt"] != null + ? dataEntryDictionary["updatedAt"].ToString() + : null, + ETag = dataEntryDictionary.ContainsKey("eTag") && dataEntryDictionary["eTag"] != null + ? dataEntryDictionary["eTag"].ToString() + : null, + ExpiresAt = dataEntryDictionary.ContainsKey("expiresAt") && dataEntryDictionary["expiresAt"] != null + ? dataEntryDictionary["expiresAt"].ToString() + : null + }; + if (dataEntryDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + data.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + result.Data.Add(data); + } + } + else if (objectDictionary.TryGetValue("meta", out var metaObject) && metaObject != null) + { + var metaDictionary = jsonPlug.ConvertToDictionaryObject(metaObject); + if (metaDictionary == null || !metaDictionary.Any()) + { + continue; + } + + var meta = new PaginationMeta(); + if (metaDictionary.TryGetValue("next_cursor", out var nextCursor) && nextCursor != null) + { + meta.NextCursor = nextCursor.ToString(); + } + + if (metaDictionary.TryGetValue("has_next", out var hasNext) && hasNext != null) + { + meta.HasNext = (bool)hasNext; + } + + if (metaDictionary.TryGetValue("limit", out var limit) && limit != null) + { + meta.Limit = (int)(long)limit; + } + + result.Meta = meta; + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntitiesListResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntitiesListResultJsonDataParse.cs new file mode 100644 index 000000000..607d6bdbd --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntitiesListResultJsonDataParse.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncEntitiesListResultJsonDataParse +{ + internal static PNDataSyncEntitiesListResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + var result = new PNDataSyncEntitiesListResult(); + foreach (var rawObject in listObject) + { + var objectDictionary = jsonPlug.ConvertToDictionaryObject(rawObject); + if (objectDictionary == null || !objectDictionary.Any()) + { + continue; + } + + if (objectDictionary.TryGetValue("data", out var dataObject) && dataObject != null) + { + var dataArray = jsonPlug.ConvertToObjectArray(dataObject); + if (dataArray == null || !dataArray.Any()) + { + continue; + } + + for (int i = 0; i < dataArray.Length; i++) + { + if (dataArray[i] is not Dictionary dataEntryDictionary) + { + continue; + } + var data = new PNDataSyncEntityResult + { + Id = dataEntryDictionary.ContainsKey("id") && dataEntryDictionary["id"] != null + ? dataEntryDictionary["id"].ToString() + : null, + EntityClass = dataEntryDictionary.ContainsKey("entityClass") && dataEntryDictionary["entityClass"] != null + ? dataEntryDictionary["entityClass"].ToString() + : null, + EntityClassVersion = dataEntryDictionary.ContainsKey("entityClassVersion") && dataEntryDictionary["entityClassVersion"] != null + ? (int)(long)dataEntryDictionary["entityClassVersion"] + : 1, + EntityClassLevel = dataEntryDictionary.ContainsKey("entityClassLevel") && dataEntryDictionary["entityClassLevel"] != null + ? dataEntryDictionary["entityClassLevel"].ToString() + : null, + Status = dataEntryDictionary.ContainsKey("status") && dataEntryDictionary["status"] != null + ? dataEntryDictionary["status"].ToString() + : null, + CreatedAt = dataEntryDictionary.ContainsKey("createdAt") && dataEntryDictionary["createdAt"] != null + ? dataEntryDictionary["createdAt"].ToString() + : null, + UpdatedAt = dataEntryDictionary.ContainsKey("updatedAt") && dataEntryDictionary["updatedAt"] != null + ? dataEntryDictionary["updatedAt"].ToString() + : null, + ETag = dataEntryDictionary.ContainsKey("eTag") && dataEntryDictionary["eTag"] != null + ? dataEntryDictionary["eTag"].ToString() + : null, + ExpiresAt = dataEntryDictionary.ContainsKey("expiresAt") && dataEntryDictionary["expiresAt"] != null + ? dataEntryDictionary["expiresAt"].ToString() + : null + }; + if (dataEntryDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + data.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + result.Data.Add(data); + } + } + else if (objectDictionary.TryGetValue("meta", out var metaObject) && metaObject != null) + { + var metaDictionary = jsonPlug.ConvertToDictionaryObject(metaObject); + if (metaDictionary == null || !metaDictionary.Any()) + { + continue; + } + + var meta = new PaginationMeta(); + if (metaDictionary.TryGetValue("next_cursor", out var nextCursor) && nextCursor != null) + { + meta.NextCursor = nextCursor.ToString(); + } + + if (metaDictionary.TryGetValue("has_next", out var hasNext) && hasNext != null) + { + meta.HasNext = (bool)hasNext; + } + + if (metaDictionary.TryGetValue("limit", out var limit) && limit != null) + { + meta.Limit = (int)(long)limit; + } + + result.Meta = meta; + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntityResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntityResultJsonDataParse.cs new file mode 100644 index 000000000..4ab6e846b --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEntityResultJsonDataParse.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncEntityResultJsonDataParse +{ + internal static PNDataSyncEntityResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + Dictionary dictionaryObject = (listObject != null && listObject.Count == 1) + ? jsonPlug.ConvertToDictionaryObject(listObject[0]) + : null; + PNDataSyncEntityResult result = null; + if (dictionaryObject != null && dictionaryObject.ContainsKey("data")) + { + result = new PNDataSyncEntityResult(); + + Dictionary dataDictionary = + jsonPlug.ConvertToDictionaryObject(dictionaryObject["data"]); + if (dataDictionary != null && dataDictionary.Count > 0) + { + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + result.EntityClass = dataDictionary.ContainsKey("entityClass") && dataDictionary["entityClass"] != null + ? dataDictionary["entityClass"].ToString() + : null; + result.EntityClassVersion = dataDictionary.ContainsKey("entityClassVersion") && dataDictionary["entityClassVersion"] != null + ? (int)(long)dataDictionary["entityClassVersion"] + : 1; + result.EntityClassLevel = dataDictionary.ContainsKey("entityClassLevel") && dataDictionary["entityClassLevel"] != null + ? dataDictionary["entityClassLevel"].ToString() + : null; + result.Status = dataDictionary.ContainsKey("status") && dataDictionary["status"] != null + ? dataDictionary["status"].ToString() + : null; + result.CreatedAt = dataDictionary.ContainsKey("createdAt") && dataDictionary["createdAt"] != null + ? dataDictionary["createdAt"].ToString() + : null; + result.UpdatedAt = dataDictionary.ContainsKey("updatedAt") && dataDictionary["updatedAt"] != null + ? dataDictionary["updatedAt"].ToString() + : null; + result.ETag = dataDictionary.ContainsKey("eTag") && dataDictionary["eTag"] != null + ? dataDictionary["eTag"].ToString() + : null; + result.ExpiresAt = dataDictionary.ContainsKey("expiresAt") && dataDictionary["expiresAt"] != null + ? dataDictionary["expiresAt"].ToString() + : null; + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + if (dataDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + result.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncEventJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEventJsonDataParse.cs new file mode 100644 index 000000000..8ebf325af --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncEventJsonDataParse.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncEventJsonDataParse +{ + internal static PNDataSyncEventResult GetObject(IJsonPluggableLibrary jsonPlug, + IDictionary jsonFields) + { + var payload = jsonFields is { Count: > 0 } + ? jsonPlug.ConvertToDictionaryObject(jsonFields["payload"]) + : null; + if (payload == null) + { + return null; + } + + var result = new PNDataSyncEventResult + { + Version = GetStringValue(payload, "version") + }; + + var metadata = payload.ContainsKey("metadata") && payload["metadata"] != null + ? jsonPlug.ConvertToDictionaryObject(payload["metadata"]) + : null; + if (metadata != null) + { + result.Event = GetStringValue(metadata, "event"); + result.Source = GetStringValue(metadata, "source"); + result.Type = GetStringValue(metadata, "type"); + result.ClassName = GetStringValue(metadata, "className"); + result.ClassLevel = GetStringValue(metadata, "classLevel"); + if (int.TryParse(GetStringValue(metadata, "classVersion"), out var classVersion)) + { + result.ClassVersion = classVersion; + } + } + + var data = payload.ContainsKey("data") && payload["data"] != null + ? jsonPlug.ConvertToDictionaryObject(payload["data"]) + : null; + if (data != null) + { + var eventName = result.Event?.ToLowerInvariant(); + var type = result.Type?.ToLowerInvariant(); + + if (eventName == "delete") + { + result.Id = GetStringValue(data, "id"); + result.DeletedAt = GetStringValue(data, "deletedAt"); + } + else if (type == "entity" || type == "user" || type == "channel") + { + result.EntityData = new PNDataSyncEntityResult + { + Id = GetStringValue(data, "id"), + EntityClass = result.ClassName, + EntityClassVersion = result.ClassVersion, + Status = GetStringValue(data, "status"), + Payload = data.ContainsKey("payload") && data["payload"] != null + ? jsonPlug.ConvertToDictionaryObject(data["payload"]) + : null, + CreatedAt = GetStringValue(data, "createdAt"), + UpdatedAt = GetStringValue(data, "updatedAt"), + ETag = GetStringValue(data, "eTag"), + ExpiresAt = GetStringValue(data, "expiresAt"), + }; + } + else if (type == "relationship" || type == "membership") + { + result.RelationshipData = new PNDataSyncRelationshipResult + { + Id = GetStringValue(data, "id"), + EntityAId = GetStringValue(data, type == "membership" ? "channelId" : "entityAId"), + EntityBId = GetStringValue(data, type == "membership" ? "userId" : "entityBId"), + RelationshipClass = result.ClassName, + RelationshipClassVersion = result.ClassVersion, + Status = GetStringValue(data, "status"), + Payload = data.ContainsKey("payload") && data["payload"] != null + ? jsonPlug.ConvertToDictionaryObject(data["payload"]) + : null, + CreatedAt = GetStringValue(data, "createdAt"), + UpdatedAt = GetStringValue(data, "updatedAt"), + ETag = GetStringValue(data, "eTag"), + ExpiresAt = GetStringValue(data, "expiresAt"), + }; + } + } + + if (long.TryParse(jsonFields["publishTimetoken"]?.ToString(), out var timestamp)) + { + result.Timestamp = timestamp; + } + + result.Subscription = jsonFields["channelGroup"]?.ToString(); + result.Channel = jsonFields["channel"]?.ToString(); + + return result; + } + + private static string GetStringValue(IDictionary dictionary, string key) + { + return dictionary.ContainsKey(key) && dictionary[key] != null + ? dictionary[key].ToString() + : null; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipResultJsonDataParse.cs new file mode 100644 index 000000000..7d116e55b --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipResultJsonDataParse.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncMembershipResultJsonDataParse +{ + internal static PNDataSyncMembershipResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + Dictionary dictionaryObject = (listObject != null && listObject.Count == 1) + ? jsonPlug.ConvertToDictionaryObject(listObject[0]) + : null; + PNDataSyncMembershipResult result = null; + if (dictionaryObject != null && dictionaryObject.ContainsKey("data")) + { + result = new PNDataSyncMembershipResult(); + + Dictionary dataDictionary = + jsonPlug.ConvertToDictionaryObject(dictionaryObject["data"]); + if (dataDictionary != null && dataDictionary.Count > 0) + { + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + result.ChannelId = dataDictionary.ContainsKey("channelId") && dataDictionary["channelId"] != null + ? dataDictionary["channelId"].ToString() + : null; + result.UserId = dataDictionary.ContainsKey("userId") && dataDictionary["userId"] != null + ? dataDictionary["userId"].ToString() + : null; + result.RelationshipClass = dataDictionary.ContainsKey("relationshipClass") && dataDictionary["relationshipClass"] != null + ? dataDictionary["relationshipClass"].ToString() + : null; + result.RelationshipClassVersion = dataDictionary.ContainsKey("relationshipClassVersion") && dataDictionary["relationshipClassVersion"] != null + ? (int)(long)dataDictionary["relationshipClassVersion"] + : 1; + result.Status = dataDictionary.ContainsKey("status") && dataDictionary["status"] != null + ? dataDictionary["status"].ToString() + : null; + result.CreatedAt = dataDictionary.ContainsKey("createdAt") && dataDictionary["createdAt"] != null + ? dataDictionary["createdAt"].ToString() + : null; + result.UpdatedAt = dataDictionary.ContainsKey("updatedAt") && dataDictionary["updatedAt"] != null + ? dataDictionary["updatedAt"].ToString() + : null; + result.ETag = dataDictionary.ContainsKey("eTag") && dataDictionary["eTag"] != null + ? dataDictionary["eTag"].ToString() + : null; + result.ExpiresAt = dataDictionary.ContainsKey("expiresAt") && dataDictionary["expiresAt"] != null + ? dataDictionary["expiresAt"].ToString() + : null; + if (dataDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + result.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipsListResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipsListResultJsonDataParse.cs new file mode 100644 index 000000000..5ea60dd14 --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncMembershipsListResultJsonDataParse.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Linq; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncMembershipsListResultJsonDataParse +{ + internal static PNDataSyncMembershipsListResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + var result = new PNDataSyncMembershipsListResult(); + foreach (var rawObject in listObject) + { + var objectDictionary = jsonPlug.ConvertToDictionaryObject(rawObject); + if (objectDictionary == null || !objectDictionary.Any()) + { + continue; + } + + if (objectDictionary.TryGetValue("data", out var dataObject) && dataObject != null) + { + var dataArray = jsonPlug.ConvertToObjectArray(dataObject); + if (dataArray == null || !dataArray.Any()) + { + continue; + } + + for (int i = 0; i < dataArray.Length; i++) + { + if (dataArray[i] is not Dictionary dataEntryDictionary) + { + continue; + } + var data = new PNDataSyncMembershipResult + { + Id = dataEntryDictionary.ContainsKey("id") && dataEntryDictionary["id"] != null + ? dataEntryDictionary["id"].ToString() + : null, + ChannelId = dataEntryDictionary.ContainsKey("channelId") && dataEntryDictionary["channelId"] != null + ? dataEntryDictionary["channelId"].ToString() + : null, + UserId = dataEntryDictionary.ContainsKey("userId") && dataEntryDictionary["userId"] != null + ? dataEntryDictionary["userId"].ToString() + : null, + RelationshipClass = dataEntryDictionary.ContainsKey("relationshipClass") && dataEntryDictionary["relationshipClass"] != null + ? dataEntryDictionary["relationshipClass"].ToString() + : null, + RelationshipClassVersion = dataEntryDictionary.ContainsKey("relationshipClassVersion") && dataEntryDictionary["relationshipClassVersion"] != null + ? (int)(long)dataEntryDictionary["relationshipClassVersion"] + : 1, + Status = dataEntryDictionary.ContainsKey("status") && dataEntryDictionary["status"] != null + ? dataEntryDictionary["status"].ToString() + : null, + CreatedAt = dataEntryDictionary.ContainsKey("createdAt") && dataEntryDictionary["createdAt"] != null + ? dataEntryDictionary["createdAt"].ToString() + : null, + UpdatedAt = dataEntryDictionary.ContainsKey("updatedAt") && dataEntryDictionary["updatedAt"] != null + ? dataEntryDictionary["updatedAt"].ToString() + : null, + ETag = dataEntryDictionary.ContainsKey("eTag") && dataEntryDictionary["eTag"] != null + ? dataEntryDictionary["eTag"].ToString() + : null, + ExpiresAt = dataEntryDictionary.ContainsKey("expiresAt") && dataEntryDictionary["expiresAt"] != null + ? dataEntryDictionary["expiresAt"].ToString() + : null + }; + if (dataEntryDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + data.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + result.Data.Add(data); + } + } + else if (objectDictionary.TryGetValue("meta", out var metaObject) && metaObject != null) + { + var metaDictionary = jsonPlug.ConvertToDictionaryObject(metaObject); + if (metaDictionary == null || !metaDictionary.Any()) + { + continue; + } + + var meta = new PaginationMeta(); + if (metaDictionary.TryGetValue("next_cursor", out var nextCursor) && nextCursor != null) + { + meta.NextCursor = nextCursor.ToString(); + } + + if (metaDictionary.TryGetValue("has_next", out var hasNext) && hasNext != null) + { + meta.HasNext = (bool)hasNext; + } + + if (metaDictionary.TryGetValue("limit", out var limit) && limit != null) + { + meta.Limit = (int)(long)limit; + } + + result.Meta = meta; + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipResultJsonDataParse.cs new file mode 100644 index 000000000..051b87fc5 --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipResultJsonDataParse.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncRelationshipResultJsonDataParse +{ + internal static PNDataSyncRelationshipResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + Dictionary dictionaryObject = (listObject != null && listObject.Count == 1) + ? jsonPlug.ConvertToDictionaryObject(listObject[0]) + : null; + PNDataSyncRelationshipResult result = null; + if (dictionaryObject != null && dictionaryObject.ContainsKey("data")) + { + result = new PNDataSyncRelationshipResult(); + + Dictionary dataDictionary = + jsonPlug.ConvertToDictionaryObject(dictionaryObject["data"]); + if (dataDictionary != null && dataDictionary.Count > 0) + { + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + result.EntityAId = dataDictionary.ContainsKey("entityAId") && dataDictionary["entityAId"] != null + ? dataDictionary["entityAId"].ToString() + : null; + result.EntityBId = dataDictionary.ContainsKey("entityBId") && dataDictionary["entityBId"] != null + ? dataDictionary["entityBId"].ToString() + : null; + result.RelationshipClass = dataDictionary.ContainsKey("relationshipClass") && dataDictionary["relationshipClass"] != null + ? dataDictionary["relationshipClass"].ToString() + : null; + result.RelationshipClassVersion = dataDictionary.ContainsKey("relationshipClassVersion") && dataDictionary["relationshipClassVersion"] != null + ? (int)(long)dataDictionary["relationshipClassVersion"] + : 1; + result.Status = dataDictionary.ContainsKey("status") && dataDictionary["status"] != null + ? dataDictionary["status"].ToString() + : null; + result.CreatedAt = dataDictionary.ContainsKey("createdAt") && dataDictionary["createdAt"] != null + ? dataDictionary["createdAt"].ToString() + : null; + result.UpdatedAt = dataDictionary.ContainsKey("updatedAt") && dataDictionary["updatedAt"] != null + ? dataDictionary["updatedAt"].ToString() + : null; + result.ETag = dataDictionary.ContainsKey("eTag") && dataDictionary["eTag"] != null + ? dataDictionary["eTag"].ToString() + : null; + result.ExpiresAt = dataDictionary.ContainsKey("expiresAt") && dataDictionary["expiresAt"] != null + ? dataDictionary["expiresAt"].ToString() + : null; + if (dataDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + result.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipsListResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipsListResultJsonDataParse.cs new file mode 100644 index 000000000..27fe9ff2e --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncRelationshipsListResultJsonDataParse.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Linq; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncRelationshipsListResultJsonDataParse +{ + internal static PNDataSyncRelationshipsListResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + var result = new PNDataSyncRelationshipsListResult(); + foreach (var rawObject in listObject) + { + var objectDictionary = jsonPlug.ConvertToDictionaryObject(rawObject); + if (objectDictionary == null || !objectDictionary.Any()) + { + continue; + } + + if (objectDictionary.TryGetValue("data", out var dataObject) && dataObject != null) + { + var dataArray = jsonPlug.ConvertToObjectArray(dataObject); + if (dataArray == null || !dataArray.Any()) + { + continue; + } + + for (int i = 0; i < dataArray.Length; i++) + { + if (dataArray[i] is not Dictionary dataEntryDictionary) + { + continue; + } + var data = new PNDataSyncRelationshipResult + { + Id = dataEntryDictionary.ContainsKey("id") && dataEntryDictionary["id"] != null + ? dataEntryDictionary["id"].ToString() + : null, + EntityAId = dataEntryDictionary.ContainsKey("entityAId") && dataEntryDictionary["entityAId"] != null + ? dataEntryDictionary["entityAId"].ToString() + : null, + EntityBId = dataEntryDictionary.ContainsKey("entityBId") && dataEntryDictionary["entityBId"] != null + ? dataEntryDictionary["entityBId"].ToString() + : null, + RelationshipClass = dataEntryDictionary.ContainsKey("relationshipClass") && dataEntryDictionary["relationshipClass"] != null + ? dataEntryDictionary["relationshipClass"].ToString() + : null, + RelationshipClassVersion = dataEntryDictionary.ContainsKey("relationshipClassVersion") && dataEntryDictionary["relationshipClassVersion"] != null + ? (int)(long)dataEntryDictionary["relationshipClassVersion"] + : 1, + Status = dataEntryDictionary.ContainsKey("status") && dataEntryDictionary["status"] != null + ? dataEntryDictionary["status"].ToString() + : null, + CreatedAt = dataEntryDictionary.ContainsKey("createdAt") && dataEntryDictionary["createdAt"] != null + ? dataEntryDictionary["createdAt"].ToString() + : null, + UpdatedAt = dataEntryDictionary.ContainsKey("updatedAt") && dataEntryDictionary["updatedAt"] != null + ? dataEntryDictionary["updatedAt"].ToString() + : null, + ETag = dataEntryDictionary.ContainsKey("eTag") && dataEntryDictionary["eTag"] != null + ? dataEntryDictionary["eTag"].ToString() + : null, + ExpiresAt = dataEntryDictionary.ContainsKey("expiresAt") && dataEntryDictionary["expiresAt"] != null + ? dataEntryDictionary["expiresAt"].ToString() + : null + }; + if (dataEntryDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + data.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + result.Data.Add(data); + } + } + else if (objectDictionary.TryGetValue("meta", out var metaObject) && metaObject != null) + { + var metaDictionary = jsonPlug.ConvertToDictionaryObject(metaObject); + if (metaDictionary == null || !metaDictionary.Any()) + { + continue; + } + + var meta = new PaginationMeta(); + if (metaDictionary.TryGetValue("next_cursor", out var nextCursor) && nextCursor != null) + { + meta.NextCursor = nextCursor.ToString(); + } + + if (metaDictionary.TryGetValue("has_next", out var hasNext) && hasNext != null) + { + meta.HasNext = (bool)hasNext; + } + + if (metaDictionary.TryGetValue("limit", out var limit) && limit != null) + { + meta.Limit = (int)(long)limit; + } + + result.Meta = meta; + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncUserResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncUserResultJsonDataParse.cs new file mode 100644 index 000000000..61206fd4d --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncUserResultJsonDataParse.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncUserResultJsonDataParse +{ + internal static PNDataSyncUserResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + Dictionary dictionaryObject = (listObject != null && listObject.Count == 1) + ? jsonPlug.ConvertToDictionaryObject(listObject[0]) + : null; + PNDataSyncUserResult result = null; + if (dictionaryObject != null && dictionaryObject.ContainsKey("data")) + { + result = new PNDataSyncUserResult(); + + Dictionary dataDictionary = + jsonPlug.ConvertToDictionaryObject(dictionaryObject["data"]); + if (dataDictionary != null && dataDictionary.Count > 0) + { + result.Id = dataDictionary.ContainsKey("id") && dataDictionary["id"] != null + ? dataDictionary["id"].ToString() + : null; + result.EntityClass = dataDictionary.ContainsKey("entityClass") && dataDictionary["entityClass"] != null + ? dataDictionary["entityClass"].ToString() + : null; + result.EntityClassVersion = dataDictionary.ContainsKey("entityClassVersion") && dataDictionary["entityClassVersion"] != null + ? (int)(long)dataDictionary["entityClassVersion"] + : 1; + result.EntityClassLevel = dataDictionary.ContainsKey("entityClassLevel") && dataDictionary["entityClassLevel"] != null + ? dataDictionary["entityClassLevel"].ToString() + : null; + result.Status = dataDictionary.ContainsKey("status") && dataDictionary["status"] != null + ? dataDictionary["status"].ToString() + : null; + result.CreatedAt = dataDictionary.ContainsKey("createdAt") && dataDictionary["createdAt"] != null + ? dataDictionary["createdAt"].ToString() + : null; + result.UpdatedAt = dataDictionary.ContainsKey("updatedAt") && dataDictionary["updatedAt"] != null + ? dataDictionary["updatedAt"].ToString() + : null; + result.ETag = dataDictionary.ContainsKey("eTag") && dataDictionary["eTag"] != null + ? dataDictionary["eTag"].ToString() + : null; + result.ExpiresAt = dataDictionary.ContainsKey("expiresAt") && dataDictionary["expiresAt"] != null + ? dataDictionary["expiresAt"].ToString() + : null; + if (dataDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + result.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/JsonDataParse/PNDataSyncUsersListResultJsonDataParse.cs b/src/Api/PubnubApi/JsonDataParse/PNDataSyncUsersListResultJsonDataParse.cs new file mode 100644 index 000000000..fab49b9b4 --- /dev/null +++ b/src/Api/PubnubApi/JsonDataParse/PNDataSyncUsersListResultJsonDataParse.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +internal static class PNDataSyncUsersListResultJsonDataParse +{ + internal static PNDataSyncUsersListResult GetObject(IJsonPluggableLibrary jsonPlug, List listObject) + { + var result = new PNDataSyncUsersListResult(); + foreach (var rawObject in listObject) + { + var objectDictionary = jsonPlug.ConvertToDictionaryObject(rawObject); + if (objectDictionary == null || !objectDictionary.Any()) + { + continue; + } + + if (objectDictionary.TryGetValue("data", out var dataObject) && dataObject != null) + { + var dataArray = jsonPlug.ConvertToObjectArray(dataObject); + if (dataArray == null || !dataArray.Any()) + { + continue; + } + + for (int i = 0; i < dataArray.Length; i++) + { + if (dataArray[i] is not Dictionary dataEntryDictionary) + { + continue; + } + var data = new PNDataSyncUserResult + { + Id = dataEntryDictionary.ContainsKey("id") && dataEntryDictionary["id"] != null + ? dataEntryDictionary["id"].ToString() + : null, + EntityClass = dataEntryDictionary.ContainsKey("entityClass") && dataEntryDictionary["entityClass"] != null + ? dataEntryDictionary["entityClass"].ToString() + : null, + EntityClassVersion = dataEntryDictionary.ContainsKey("entityClassVersion") && dataEntryDictionary["entityClassVersion"] != null + ? (int)(long)dataEntryDictionary["entityClassVersion"] + : 1, + EntityClassLevel = dataEntryDictionary.ContainsKey("entityClassLevel") && dataEntryDictionary["entityClassLevel"] != null + ? dataEntryDictionary["entityClassLevel"].ToString() + : null, + Status = dataEntryDictionary.ContainsKey("status") && dataEntryDictionary["status"] != null + ? dataEntryDictionary["status"].ToString() + : null, + CreatedAt = dataEntryDictionary.ContainsKey("createdAt") && dataEntryDictionary["createdAt"] != null + ? dataEntryDictionary["createdAt"].ToString() + : null, + UpdatedAt = dataEntryDictionary.ContainsKey("updatedAt") && dataEntryDictionary["updatedAt"] != null + ? dataEntryDictionary["updatedAt"].ToString() + : null, + ETag = dataEntryDictionary.ContainsKey("eTag") && dataEntryDictionary["eTag"] != null + ? dataEntryDictionary["eTag"].ToString() + : null, + ExpiresAt = dataEntryDictionary.ContainsKey("expiresAt") && dataEntryDictionary["expiresAt"] != null + ? dataEntryDictionary["expiresAt"].ToString() + : null + }; + if (dataEntryDictionary.TryGetValue("payload", out var payloadObject) && payloadObject != null) + { + data.Payload = jsonPlug.ConvertToDictionaryObject(payloadObject); + } + result.Data.Add(data); + } + } + else if (objectDictionary.TryGetValue("meta", out var metaObject) && metaObject != null) + { + var metaDictionary = jsonPlug.ConvertToDictionaryObject(metaObject); + if (metaDictionary == null || !metaDictionary.Any()) + { + continue; + } + + var meta = new PaginationMeta(); + if (metaDictionary.TryGetValue("next_cursor", out var nextCursor) && nextCursor != null) + { + meta.NextCursor = nextCursor.ToString(); + } + + if (metaDictionary.TryGetValue("has_next", out var hasNext) && hasNext != null) + { + meta.HasNext = (bool)hasNext; + } + + if (metaDictionary.TryGetValue("limit", out var limit) && limit != null) + { + meta.Limit = (int)(long)limit; + } + + result.Meta = meta; + } + } + + return result; + } +} diff --git a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncProjections.cs b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncProjections.cs new file mode 100644 index 000000000..459ea5038 --- /dev/null +++ b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncProjections.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace PubnubApi +{ + /// + /// Per-resource projection assignment for Data Sync. + /// The value is the single projection name the principal is "looking through" + /// for that resource (use "__default__" for the base projection). + /// These buckets assign projections only; the matching permissions for users and channels + /// come from the shared Uuids/Users and Channels/Spaces mappings, not from PNDataSyncTokenScopes. + /// + public class PNDataSyncProjectionScope + { + /// + /// Entity id -> projection name. Key e.g. "order-456" (resources) or "order-*" (patterns). + /// + public Dictionary Entities { get; set; } + + /// + /// User id -> projection name. Key e.g. "alice" (resources) or "user-*" (patterns). + /// + public Dictionary Users { get; set; } + + /// + /// Channel id -> projection name. Key e.g. "general" (resources) or "channel-*" (patterns). + /// + public Dictionary Channels { get; set; } + + /// + /// Relationship id -> projection name. Key e.g. "user.A:channel.X". + /// + public Dictionary Relationships { get; set; } + + /// + /// Membership id -> projection name. + /// + public Dictionary Memberships { get; set; } + } + + /// + /// Data Sync projection permissions for grant token requests. + /// Encoded into the "pn-projections" key within the token's meta section. + /// Exact (res) match takes priority over pattern (pat) match. + /// + public class PNDataSyncProjections + { + /// + /// Projection assignments for specific Data Sync resources (by exact id). + /// + public PNDataSyncProjectionScope Resources { get; set; } + + /// + /// Projection assignments matching Data Sync resources by regex pattern. + /// + public PNDataSyncProjectionScope Patterns { get; set; } + } +} diff --git a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncTokenScopes.cs b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncTokenScopes.cs new file mode 100644 index 000000000..90ec9da52 --- /dev/null +++ b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNDataSyncTokenScopes.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace PubnubApi +{ + /// + /// Optional DataSync permission scopes. Serialized to the REST keys + /// datasync:entities / datasync:relationships / datasync:memberships + /// inside the grant token resources / patterns sections. + /// + public class PNDataSyncTokenScopes + { + /// + /// Concrete or pattern entity ids, e.g. "order-456" (resources) or "order-*" (patterns). + /// + public Dictionary Entities { get; set; } + + /// + /// Composite relationship ids, e.g. "user.A:channel.X". + /// + public Dictionary Relationships { get; set; } + + /// + /// Composite membership ids, e.g. "user-123:channel-X". + /// + public Dictionary Memberships { get; set; } + } +} diff --git a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenContent.cs b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenContent.cs index 59cde477e..f54f341a0 100644 --- a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenContent.cs +++ b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenContent.cs @@ -19,6 +19,11 @@ public class PNTokenContent public Dictionary Meta { get; set; } + /// + /// Data Sync projection assignments decoded from the token's meta "pn-projections" section. + /// + public PNDataSyncProjections Projections { get; set; } + public string AuthorizedUuid { get; set; } public string Signature { get; set; } diff --git a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenPermissionMappingBase.cs b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenPermissionMappingBase.cs index a62a626d3..a4ef4a0a5 100644 --- a/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenPermissionMappingBase.cs +++ b/src/Api/PubnubApi/Model/Consumer/AccessManager/PNTokenPermissionMappingBase.cs @@ -21,6 +21,11 @@ public class PNTokenPermissionMappingBase public Dictionary Users { get; set; } public Dictionary Spaces { get; set; } + /// + /// Optional DataSync permission scopes (entities, relationships, memberships). + /// + public PNDataSyncTokenScopes DataSync { get; set; } + } } diff --git a/src/Api/PubnubApi/Model/Consumer/PNStatus.cs b/src/Api/PubnubApi/Model/Consumer/PNStatus.cs index 361ece757..2f2c17bd3 100644 --- a/src/Api/PubnubApi/Model/Consumer/PNStatus.cs +++ b/src/Api/PubnubApi/Model/Consumer/PNStatus.cs @@ -521,6 +521,66 @@ public void Retry() } } break; + case PNOperationType.PNDataSyncCreateEntity: + if (savedEndpointOperation is CreateEntityOperation) + { + CreateEntityOperation endpoint = savedEndpointOperation as CreateEntityOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; + case PNOperationType.PNDataSyncGetEntity: + if (savedEndpointOperation is GetEntityOperation) + { + GetEntityOperation endpoint = savedEndpointOperation as GetEntityOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; + case PNOperationType.PNDataSyncGetEntities: + if (savedEndpointOperation is GetEntitiesOperation) + { + GetEntitiesOperation endpoint = savedEndpointOperation as GetEntitiesOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; + case PNOperationType.PNDataSyncUpdateEntity: + if (savedEndpointOperation is SetEntityOperation) + { + SetEntityOperation endpoint = savedEndpointOperation as SetEntityOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; + case PNOperationType.PNDataSyncPatchEntity: + if (savedEndpointOperation is UpdateEntityOperation) + { + UpdateEntityOperation endpoint = savedEndpointOperation as UpdateEntityOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; + case PNOperationType.PNDataSyncDeleteEntity: + if (savedEndpointOperation is DeleteEntityOperation) + { + DeleteEntityOperation endpoint = savedEndpointOperation as DeleteEntityOperation; + if (endpoint != null) + { + endpoint.Retry(); + } + } + break; default: break; } diff --git a/src/Api/PubnubApi/Model/Consumer/Pubsub/PNDataSyncEventResult.cs b/src/Api/PubnubApi/Model/Consumer/Pubsub/PNDataSyncEventResult.cs new file mode 100644 index 000000000..df40c6125 --- /dev/null +++ b/src/Api/PubnubApi/Model/Consumer/Pubsub/PNDataSyncEventResult.cs @@ -0,0 +1,20 @@ +namespace PubnubApi +{ + public class PNDataSyncEventResult + { + public string Version { get; internal set; } = ""; //e.g. "3.0" + public string Event { get; internal set; } = ""; //values = create/update/delete + public string Source { get; internal set; } = ""; //values = "data-sync" + public string Type { get; internal set; } = ""; //values = entity/relationship + public string ClassName { get; internal set; } = ""; + public int ClassVersion { get; internal set; } + public string ClassLevel { get; internal set; } = ""; //values = Global/SubKey; distinguishes built-in classes from developer-defined ones of the same name + public PubnubApi.EndPoint.PNDataSyncEntityResult EntityData { get; internal set; } //Populated when Type = entity/user/channel (create/update) + public PubnubApi.EndPoint.PNDataSyncRelationshipResult RelationshipData { get; internal set; } //Populated when Type = relationship/membership (create/update) + public string Id { get; internal set; } //Populated for delete events + public string DeletedAt { get; internal set; } //Populated for delete events + public long Timestamp { get; internal set; } + public string Channel { get; internal set; } = ""; //Subscribed channel + public string Subscription { get; internal set; } + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelResultExt.cs new file mode 100644 index 000000000..cde8b93be --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncChannelResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncChannelResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncChannelResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelsListResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelsListResultExt.cs new file mode 100644 index 000000000..b4c16c9b5 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncChannelsListResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncChannelsListResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncChannelsListResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncChannelsListResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteChannelResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteChannelResultExt.cs new file mode 100644 index 000000000..bdeddb2fc --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteChannelResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncDeleteChannelResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncDeleteChannelResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncDeleteChannelResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteEntityResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteEntityResultExt.cs new file mode 100644 index 000000000..024f9ee2d --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteEntityResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncDeleteEntityResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncDeleteEntityResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncDeleteEntityResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteMembershipResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteMembershipResultExt.cs new file mode 100644 index 000000000..8eae15f92 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteMembershipResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncDeleteMembershipResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncDeleteMembershipResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncDeleteMembershipResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteRelationshipResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteRelationshipResultExt.cs new file mode 100644 index 000000000..1542e4a7a --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteRelationshipResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncDeleteRelationshipResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncDeleteRelationshipResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncDeleteRelationshipResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteUserResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteUserResultExt.cs new file mode 100644 index 000000000..2ba6acdb2 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncDeleteUserResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncDeleteUserResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncDeleteUserResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncDeleteUserResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntitiesListResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntitiesListResultExt.cs new file mode 100644 index 000000000..c4938c3c8 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntitiesListResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncEntitiesListResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncEntitiesListResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncEntitiesListResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntityResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntityResultExt.cs new file mode 100644 index 000000000..76363d3a4 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncEntityResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncEntityResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncEntityResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncEntityResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} \ No newline at end of file diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipResultExt.cs new file mode 100644 index 000000000..3b1ea7788 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncMembershipResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncMembershipResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncMembershipResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipsListResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipsListResultExt.cs new file mode 100644 index 000000000..1c12cf3bf --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncMembershipsListResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncMembershipsListResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncMembershipsListResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncMembershipsListResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipResultExt.cs new file mode 100644 index 000000000..072c2906a --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncRelationshipResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncRelationshipResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncRelationshipResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipsListResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipsListResultExt.cs new file mode 100644 index 000000000..0ae2fe17f --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncRelationshipsListResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncRelationshipsListResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncRelationshipsListResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncRelationshipsListResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUserResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUserResultExt.cs new file mode 100644 index 000000000..95d91a194 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUserResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncUserResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncUserResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncUserResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUsersListResultExt.cs b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUsersListResultExt.cs new file mode 100644 index 000000000..df2016690 --- /dev/null +++ b/src/Api/PubnubApi/Model/Derived/DataSync/PNDataSyncUsersListResultExt.cs @@ -0,0 +1,19 @@ +using System; +using PubnubApi.EndPoint; + +namespace PubnubApi; + +public class PNDataSyncUsersListResultExt : PNCallback +{ + readonly Action callbackAction; + + public PNDataSyncUsersListResultExt(Action callback) + { + this.callbackAction = callback; + } + + public override void OnResponse(PNDataSyncUsersListResult result, PNStatus status) + { + callbackAction?.Invoke(result, status); + } +} diff --git a/src/Api/PubnubApi/Model/Derived/Pubsub/SubscribeCallbackExt.cs b/src/Api/PubnubApi/Model/Derived/Pubsub/SubscribeCallbackExt.cs index 0f1d91a12..b83b65ec8 100644 --- a/src/Api/PubnubApi/Model/Derived/Pubsub/SubscribeCallbackExt.cs +++ b/src/Api/PubnubApi/Model/Derived/Pubsub/SubscribeCallbackExt.cs @@ -14,6 +14,7 @@ public class SubscribeCallbackExt : SubscribeCallback public Action> signalAction; public Action statusAction; public Action objectEventAction; + public Action dataSyncEventAction; public Action messageAction; public Action fileAction; @@ -95,6 +96,17 @@ public SubscribeCallbackExt(Action objectEventCallb fileAction = null; } + public SubscribeCallbackExt(Action dataSyncEventCallback, Action statusCallback) + { + subscribeAction = null; + presenceAction = null; + signalAction = null; + statusAction = statusCallback; + objectEventAction = null; + dataSyncEventAction = dataSyncEventCallback; + fileAction = null; + } + public SubscribeCallbackExt(Action messageActionCallback, Action statusCallback) { subscribeAction = null; @@ -189,6 +201,25 @@ public SubscribeCallbackExt(Action> messageCallb messageAction = messageActionCallback; fileAction = fileCallback; } + + public SubscribeCallbackExt(Action> messageCallback, + Action presenceCallback, + Action> signalCallback, + Action objectEventCallback, + Action dataSyncEventCallback, + Action messageActionCallback, + Action fileCallback, + Action statusCallback) + { + subscribeAction = messageCallback; + presenceAction = presenceCallback; + statusAction = statusCallback; + signalAction = signalCallback; + objectEventAction = objectEventCallback; + dataSyncEventAction = dataSyncEventCallback; + messageAction = messageActionCallback; + fileAction = fileCallback; + } public override void Message(Pubnub pubnub, PNMessageResult message) { @@ -233,6 +264,11 @@ public override void ObjectEvent(Pubnub pubnub, PNObjectEventResult objectEvent) objectEventAction?.Invoke(pubnub, objectEvent); } + public override void DataSyncEvent(Pubnub pubnub, PNDataSyncEventResult dataSyncEvent) + { + dataSyncEventAction?.Invoke(pubnub, dataSyncEvent); + } + public override void MessageAction(Pubnub pubnub, PNMessageActionEventResult messageActionEvent) { messageAction?.Invoke(pubnub, messageActionEvent); diff --git a/src/Api/PubnubApi/Properties/AssemblyInfo.cs b/src/Api/PubnubApi/Properties/AssemblyInfo.cs index 9f95298f0..9ec5a35d2 100644 --- a/src/Api/PubnubApi/Properties/AssemblyInfo.cs +++ b/src/Api/PubnubApi/Properties/AssemblyInfo.cs @@ -11,8 +11,8 @@ [assembly: AssemblyProduct("Pubnub C# SDK")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("8.3.4")] -[assembly: AssemblyFileVersion("8.3.4")] +[assembly: AssemblyVersion("9.0.0")] +[assembly: AssemblyFileVersion("9.0.0")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. diff --git a/src/Api/PubnubApi/Pubnub.cs b/src/Api/PubnubApi/Pubnub.cs index a1c1ea41f..503757737 100644 --- a/src/Api/PubnubApi/Pubnub.cs +++ b/src/Api/PubnubApi/Pubnub.cs @@ -1203,6 +1203,8 @@ public void SetLogger(IPubnubLogger logger) #region "Properties" + public DataSync DataSync { get; } + public IPubnubUnitTest PubnubUnitTest { get => pubnubUnitTest; @@ -1283,6 +1285,7 @@ public Pubnub(PNConfiguration config, IHttpClientService httpTransportService = httpTransportService ?? new HttpClientService(proxy: config.Proxy, enableHttp2: config.EnableHttp2); httpClientService.SetLogger(logger); transportMiddleware = middleware ?? new Middleware(httpClientService, config, this, tokenManager); + DataSync = new DataSync(this, pubnubUnitTest, tokenManager); logger?.Debug(() => GetConfigurationLogString(config)); } diff --git a/src/Api/PubnubApi/PubnubApi.csproj b/src/Api/PubnubApi/PubnubApi.csproj index b455db7c0..609b11afc 100644 --- a/src/Api/PubnubApi/PubnubApi.csproj +++ b/src/Api/PubnubApi/PubnubApi.csproj @@ -14,7 +14,7 @@ Pubnub - 8.3.4 + 9.0.0 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -22,9 +22,7 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Fixed issue related to Reconnect to retain subscription state. -Removed value cap from maxRetry in Linear/Exponential policies. -Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures. + Added Data Sync feature support. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously diff --git a/src/Api/PubnubApi/PubnubCoreBase.cs b/src/Api/PubnubApi/PubnubCoreBase.cs index 7d14a3580..7a778c832 100644 --- a/src/Api/PubnubApi/PubnubCoreBase.cs +++ b/src/Api/PubnubApi/PubnubCoreBase.cs @@ -621,6 +621,25 @@ private void ResponseToUserCallback(List result, PNOperationType type continue; } } + else if (currentMessage.MessageType == 5) //DataSync events + { + Dictionary dataSyncDic = payload as Dictionary; + Dictionary dataSyncMetadata = dataSyncDic != null + && dataSyncDic.ContainsKey("metadata") && dataSyncDic["metadata"] != null + ? jsonLib.ConvertToDictionaryObject(dataSyncDic["metadata"]) + : null; + if (dataSyncMetadata != null + && dataSyncMetadata.ContainsKey("source") + && dataSyncMetadata["source"].ToString() == "data-sync") + { + jsonFields.Add("payload", payload); + } + else + { + LoggingMethod.WriteToLog(currentLog, $"[{DateTime.Now.ToString(CultureInfo.InvariantCulture)}] ResponseToUserCallback - MessageType = 5 but NOT valid DataSync format to process", currentConfig.LogVerbosity); + continue; + } + } else { if ((currentConfig.CryptoModule != null || currentConfig.CipherKey.Length > 0) && currentMessage.MessageType != 1) //decrypt the subscriber message if cipherkey is available @@ -767,6 +786,15 @@ private void ResponseToUserCallback(List result, PNOperationType type Announce(fileMessage); } } + else if (currentMessage.MessageType == 5) + { + ResponseBuilder responseBuilder = new ResponseBuilder(currentConfig, jsonLib); + PNDataSyncEventResult dataSyncEvent = responseBuilder.GetEventResultObject(jsonFields); + if (dataSyncEvent != null) + { + Announce(dataSyncEvent); + } + } else if (currentMessageChannel.Contains("-pnpres")) { ResponseBuilder responseBuilder = new ResponseBuilder(currentConfig, jsonLib); @@ -1055,6 +1083,20 @@ protected PNStatus GetStatusIfError(RequestState asyncRequestState, string } } } + else if (deserializeStatus.TryGetValue("errors", out var errorListObject)) + { + var aggregateErrorsString = errorListObject.ToString(); + if (pubnubConfig.TryGetValue(PubnubInstance.InstanceId, out currentConfig)) + { + statusCode = asyncRequestState?.Response?.StatusCode ?? 500; + status = new StatusBuilder(currentConfig, jsonLib).CreateStatusResponse( + type, + PNStatusCategory.PNUnknownCategory, + asyncRequestState, + statusCode, + new PNException(aggregateErrorsString)); + } + } } else if (jsonString.ToLowerInvariant().TrimStart().IndexOf(" callbackList = SubscribeCallbackListenerList[PubnubInstance.InstanceId]; + for (int listenerIndex = 0; listenerIndex < callbackList.Count; listenerIndex++) + { + try + { + callbackList[listenerIndex].DataSyncEvent(PubnubInstance, dataSyncEvent); + } + catch (Exception ex) + { + logger?.Error($"error during event handler function, {ex.Message}"); + } + } + } + } + internal void Announce(PNMessageActionEventResult messageActionEvent) { if (PubnubInstance != null && SubscribeCallbackListenerList.ContainsKey(PubnubInstance.InstanceId)) diff --git a/src/Api/PubnubApi/Transport/HttpClientService.cs b/src/Api/PubnubApi/Transport/HttpClientService.cs index d59fb1208..a0423821a 100644 --- a/src/Api/PubnubApi/Transport/HttpClientService.cs +++ b/src/Api/PubnubApi/Transport/HttpClientService.cs @@ -1,5 +1,7 @@ -using System; +using System; +using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using System.Linq; using System.Net; @@ -80,15 +82,11 @@ public async Task GetRequest(TransportRequest transportReques { HttpRequestMessage requestMessage = new HttpRequestMessage(method: HttpMethod.Get, requestUri: transportRequest.RequestUrl); + + ApplyHeaders(requestMessage.Headers, transportRequest.Headers); ConfigureHttpVersion(requestMessage); - if (transportRequest.Headers.Keys.Count > 0) - { - foreach (var kvp in transportRequest.Headers) - { - requestMessage.Headers.Add(kvp.Key, kvp.Value); - } - } transportLogger?.Request(transportRequest); + if (transportRequest.Timeout.HasValue) { ctsWithTimeout = @@ -140,29 +138,27 @@ public async Task PostRequest(TransportRequest transportReque HttpContent postData = null; if (!string.IsNullOrEmpty(transportRequest.BodyContentString)) { - postData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8, "application/json"); + var contentType = "application/json"; + if (transportRequest.Headers.TryGetValue("Content-Type", out var ct)) + { + contentType = ct; + } + postData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8); + postData.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(contentType); } else if (transportRequest.BodyContentBytes != null) { postData = new ByteArrayContent(transportRequest.BodyContentBytes); - foreach (var transportRequestHeader in transportRequest.Headers) - { - postData.Headers.Add(transportRequestHeader.Key, transportRequestHeader.Value); - } + ApplyHeaders(postData.Headers, transportRequest.Headers); } HttpRequestMessage requestMessage = new HttpRequestMessage(method: HttpMethod.Post, requestUri: transportRequest.RequestUrl) { Content = postData }; + + ApplyHeaders(requestMessage.Headers, transportRequest.Headers, excludeKey: "Content-Type"); ConfigureHttpVersion(requestMessage); - // Set Http Request header, When the header is not a payload content header. - if (transportRequest.Headers.Keys.Count > 0 && transportRequest.BodyContentBytes == null) - { - foreach (var kvp in transportRequest.Headers) - { - requestMessage.Headers.Add(kvp.Key, kvp.Value); - } - } transportLogger?.Request(transportRequest); + if (transportRequest.Timeout.HasValue) { ctsWithTimeout = @@ -214,30 +210,28 @@ public async Task PutRequest(TransportRequest transportReques if (!string.IsNullOrEmpty(transportRequest.BodyContentString)) { - putData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8, "application/json"); + var contentType = "application/json"; + if (transportRequest.Headers.TryGetValue("Content-Type", out var ct)) + { + contentType = ct; + } + putData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8); + putData.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(contentType); } else if (transportRequest.BodyContentBytes != null) { putData = new ByteArrayContent(transportRequest.FormData); - foreach (var transportRequestHeader in transportRequest.Headers) - { - putData.Headers.Add(transportRequestHeader.Key, transportRequestHeader.Value); - } + ApplyHeaders(putData.Headers, transportRequest.Headers); } HttpRequestMessage requestMessage = new HttpRequestMessage(method: HttpMethod.Put, requestUri: transportRequest.RequestUrl) { Content = putData }; - ConfigureHttpVersion(requestMessage); - if (transportRequest.Headers.Keys.Count > 0) - { - foreach (var kvp in transportRequest.Headers) - { - requestMessage.Headers.Add(kvp.Key, kvp.Value); - } - } + ApplyHeaders(requestMessage.Headers, transportRequest.Headers, excludeKey: "Content-Type"); + ConfigureHttpVersion(requestMessage); transportLogger?.Request(transportRequest); + if (transportRequest.Timeout.HasValue) { ctsWithTimeout = @@ -287,16 +281,11 @@ public async Task DeleteRequest(TransportRequest transportReq { HttpRequestMessage requestMessage = new HttpRequestMessage(method: HttpMethod.Delete, requestUri: transportRequest.RequestUrl); + + ApplyHeaders(requestMessage.Headers, transportRequest.Headers); ConfigureHttpVersion(requestMessage); - if (transportRequest.Headers.Keys.Count > 0) - { - foreach (var kvp in transportRequest.Headers) - { - requestMessage.Headers.Add(kvp.Key, kvp.Value); - } - } - transportLogger?.Request(transportRequest); + if (transportRequest.Timeout.HasValue) { ctsWithTimeout = @@ -348,31 +337,28 @@ public async Task PatchRequest(TransportRequest transportRequ if (!string.IsNullOrEmpty(transportRequest.BodyContentString)) { - patchData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8, - "application/json"); + var contentType = "application/json"; + if (transportRequest.Headers.TryGetValue("Content-Type", out var ct)) + { + contentType = ct; + } + patchData = new StringContent(transportRequest.BodyContentString, Encoding.UTF8); + patchData.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(contentType); } else if (transportRequest.BodyContentBytes != null) { patchData = new ByteArrayContent(transportRequest.FormData); - foreach (var transportRequestHeader in transportRequest.Headers) - { - patchData.Headers.Add(transportRequestHeader.Key, transportRequestHeader.Value); - } + ApplyHeaders(patchData.Headers, transportRequest.Headers); } HttpRequestMessage requestMessage = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri: transportRequest.RequestUrl) { Content = patchData }; - ConfigureHttpVersion(requestMessage); - if (transportRequest.Headers.Keys.Count > 0) - { - foreach (var kvp in transportRequest.Headers) - { - requestMessage.Headers.Add(kvp.Key, $"\"{kvp.Value}\""); - } - } + ApplyHeaders(requestMessage.Headers, transportRequest.Headers, excludeKey: "Content-Type"); + ConfigureHttpVersion(requestMessage); transportLogger?.Request(transportRequest); + if (transportRequest.Timeout.HasValue) { ctsWithTimeout = @@ -413,6 +399,34 @@ public async Task PatchRequest(TransportRequest transportRequ return transportResponse; } + + //This is because server returns eTag in the "someetag" format instead of "\"someetag\"" which is technically wrong, + //meaning that HttpHeaders.Add will throw a System.FormattingException, necessitating the usage of TryAddWithoutValidation() in these cases + private static readonly HashSet HeadersWithRelaxedValidation = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "If-Match", + "If-None-Match" + }; + private static void ApplyHeaders(HttpHeaders target, Dictionary source, string excludeKey = null) + { + foreach (var kvp in source) + { + if (excludeKey != null && string.Equals(kvp.Key, excludeKey, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (HeadersWithRelaxedValidation.Contains(kvp.Key)) + { + target.TryAddWithoutValidation(kvp.Key, kvp.Value); + } + else + { + target.Add(kvp.Key, kvp.Value); + } + } + } + private TransportResponse GetTransportResponseForTaskCancelation(TransportRequest transportRequest, TaskCanceledException taskCanceledException, CancellationTokenSource ctsWithTimeout) { diff --git a/src/Api/PubnubApiPCL/PubnubApiPCL.csproj b/src/Api/PubnubApiPCL/PubnubApiPCL.csproj index a45e34c8b..d62b53cd8 100644 --- a/src/Api/PubnubApiPCL/PubnubApiPCL.csproj +++ b/src/Api/PubnubApiPCL/PubnubApiPCL.csproj @@ -14,7 +14,7 @@ PubnubPCL - 8.3.4 + 9.0.0 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -22,9 +22,7 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Fixed issue related to Reconnect to retain subscription state. -Removed value cap from maxRetry in Linear/Exponential policies. -Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures. + Added Data Sync feature support. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously @@ -81,6 +79,219 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea EndPoint\ChannelGroup\RemoveChannelsFromChannelGroupOperation.cs + + EndPoint\DataSync\Channel\CreateChannelOperation.cs + + + EndPoint\DataSync\Channel\CreateChannelParameters.cs + + + EndPoint\DataSync\Channel\DeleteChannelOperation.cs + + + EndPoint\DataSync\Channel\DeleteChannelParameters.cs + + + EndPoint\DataSync\Channel\GetChannelOperation.cs + + + EndPoint\DataSync\Channel\GetChannelParameters.cs + + + EndPoint\DataSync\Channel\GetChannelsOperation.cs + + + EndPoint\DataSync\Channel\GetChannelsParameters.cs + + + EndPoint\DataSync\Channel\PNDataSyncChannelResult.cs + + + EndPoint\DataSync\Channel\PNDataSyncDeleteChannelResult.cs + + + EndPoint\DataSync\Channel\SetChannelOperation.cs + + + EndPoint\DataSync\Channel\UpdateChannelOperation.cs + + + EndPoint\DataSync\Channel\SetChannelParameters.cs + + + EndPoint\DataSync\Channel\UpdateChannelParameters.cs + + + EndPoint\DataSync\DataSync.cs + + + EndPoint\DataSync\Entity\CreateEntityOperation.cs + + + EndPoint\DataSync\Entity\CreateEntityParameters.cs + + + EndPoint\DataSync\Entity\DeleteEntityOperation.cs + + + EndPoint\DataSync\Entity\DeleteEntityParameters.cs + + + EndPoint\DataSync\Entity\GetEntitiesOperation.cs + + + EndPoint\DataSync\Entity\GetEntitiesParameters.cs + + + EndPoint\DataSync\Entity\GetEntityOperation.cs + + + EndPoint\DataSync\Entity\GetEntityParameters.cs + + + EndPoint\DataSync\Entity\PNDataSyncDeleteEntityResult.cs + + + EndPoint\DataSync\Entity\PNDataSyncEntityResult.cs + + + EndPoint\DataSync\Entity\SetEntityOperation.cs + + + EndPoint\DataSync\Entity\SetEntityParameters.cs + + + EndPoint\DataSync\Entity\UpdateEntityOperation.cs + + + EndPoint\DataSync\Entity\UpdateEntityParameters.cs + + + EndPoint\DataSync\Membership\CreateMembershipOperation.cs + + + EndPoint\DataSync\Membership\CreateMembershipParameters.cs + + + EndPoint\DataSync\Membership\DeleteMembershipOperation.cs + + + EndPoint\DataSync\Membership\DeleteMembershipParameters.cs + + + EndPoint\DataSync\Membership\GetMembershipOperation.cs + + + EndPoint\DataSync\Membership\GetMembershipParameters.cs + + + EndPoint\DataSync\Membership\GetMembershipsOperation.cs + + + EndPoint\DataSync\Membership\GetMembershipsParameters.cs + + + EndPoint\DataSync\Membership\PNDataSyncDeleteMembershipResult.cs + + + EndPoint\DataSync\Membership\PNDataSyncMembershipResult.cs + + + EndPoint\DataSync\Membership\SetMembershipOperation.cs + + + EndPoint\DataSync\Membership\SetMembershipParameters.cs + + + EndPoint\DataSync\Membership\UpdateMembershipOperation.cs + + + EndPoint\DataSync\Membership\UpdateMembershipParameters.cs + + + EndPoint\DataSync\Relationship\CreateRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\CreateRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\DeleteRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\DeleteRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\GetRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\GetRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\GetRelationshipsOperation.cs + + + EndPoint\DataSync\Relationship\GetRelationshipsParameters.cs + + + EndPoint\DataSync\Relationship\PNDataSyncDeleteRelationshipResult.cs + + + EndPoint\DataSync\Relationship\PNDataSyncRelationshipResult.cs + + + EndPoint\DataSync\Relationship\SetRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\SetRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\UpdateRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\UpdateRelationshipParameters.cs + + + EndPoint\DataSync\User\CreateUserOperation.cs + + + EndPoint\DataSync\User\CreateUserParameters.cs + + + EndPoint\DataSync\User\DeleteUserOperation.cs + + + EndPoint\DataSync\User\DeleteUserParameters.cs + + + EndPoint\DataSync\User\GetUserOperation.cs + + + EndPoint\DataSync\User\GetUserParameters.cs + + + EndPoint\DataSync\User\GetUsersOperation.cs + + + EndPoint\DataSync\User\GetUsersParameters.cs + + + EndPoint\DataSync\User\PNDataSyncDeleteUserResult.cs + + + EndPoint\DataSync\User\PNDataSyncUserResult.cs + + + EndPoint\DataSync\User\SetUserOperation.cs + + + EndPoint\DataSync\User\SetUserParameters.cs + + + EndPoint\DataSync\User\UpdateUserOperation.cs + + + EndPoint\DataSync\User\UpdateUserParameters.cs + @@ -293,6 +504,39 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + JsonDataParse\PNDataSyncChannelResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncChannelsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEntitiesListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEntityResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEventJsonDataParse.cs + + + JsonDataParse\PNDataSyncMembershipResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncMembershipsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncRelationshipResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncRelationshipsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncUserResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncUsersListResultJsonDataParse.cs + @@ -336,6 +580,12 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + Model\Consumer\AccessManager\PNDataSyncProjections.cs + + + Model\Consumer\AccessManager\PNDataSyncTokenScopes.cs + @@ -422,6 +672,9 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea Model\Consumer\Presence\PNWhereNowResult.cs + + Model\Consumer\Pubsub\PNDataSyncEventResult.cs + @@ -475,6 +728,51 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea Model\Derived\ChannelGroup\PNChannelGroupsRemoveChannelResultExt.cs + + Model\Derived\DataSync\PNDataSyncChannelResultExt.cs + + + Model\Derived\DataSync\PNDataSyncChannelsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteChannelResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteEntityResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteMembershipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteRelationshipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteUserResultExt.cs + + + Model\Derived\DataSync\PNDataSyncEntitiesListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncEntityResultExt.cs + + + Model\Derived\DataSync\PNDataSyncMembershipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncMembershipsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncRelationshipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncRelationshipsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncUserResultExt.cs + + + Model\Derived\DataSync\PNDataSyncUsersListResultExt.cs + @@ -641,6 +939,8 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + @@ -673,14 +973,12 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea - - @@ -699,5 +997,10 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + + + + diff --git a/src/Api/PubnubApiUWP/PubnubApiUWP.csproj b/src/Api/PubnubApiUWP/PubnubApiUWP.csproj index c33d75fd7..207f9bec5 100644 --- a/src/Api/PubnubApiUWP/PubnubApiUWP.csproj +++ b/src/Api/PubnubApiUWP/PubnubApiUWP.csproj @@ -16,7 +16,7 @@ PubnubUWP - 8.3.4 + 9.0.0 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -24,9 +24,7 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Fixed issue related to Reconnect to retain subscription state. -Removed value cap from maxRetry in Linear/Exponential policies. -Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures. + Added Data Sync feature support. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously @@ -206,6 +204,219 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea EndPoint\ChannelGroup\RemoveChannelsFromChannelGroupOperation.cs + + EndPoint\DataSync\Channel\CreateChannelOperation.cs + + + EndPoint\DataSync\Channel\CreateChannelParameters.cs + + + EndPoint\DataSync\Channel\DeleteChannelOperation.cs + + + EndPoint\DataSync\Channel\DeleteChannelParameters.cs + + + EndPoint\DataSync\Channel\GetChannelOperation.cs + + + EndPoint\DataSync\Channel\GetChannelParameters.cs + + + EndPoint\DataSync\Channel\GetChannelsOperation.cs + + + EndPoint\DataSync\Channel\GetChannelsParameters.cs + + + EndPoint\DataSync\Channel\PNDataSyncChannelResult.cs + + + EndPoint\DataSync\Channel\PNDataSyncDeleteChannelResult.cs + + + EndPoint\DataSync\Channel\SetChannelOperation.cs + + + EndPoint\DataSync\Channel\SetChannelParameters.cs + + + EndPoint\DataSync\Channel\UpdateChannelOperation.cs + + + EndPoint\DataSync\Channel\UpdateChannelParameters.cs + + + EndPoint\DataSync\DataSync.cs + + + EndPoint\DataSync\Entity\CreateEntityOperation.cs + + + EndPoint\DataSync\Entity\CreateEntityParameters.cs + + + EndPoint\DataSync\Entity\DeleteEntityOperation.cs + + + EndPoint\DataSync\Entity\DeleteEntityParameters.cs + + + EndPoint\DataSync\Entity\GetEntitiesOperation.cs + + + EndPoint\DataSync\Entity\GetEntitiesParameters.cs + + + EndPoint\DataSync\Entity\GetEntityOperation.cs + + + EndPoint\DataSync\Entity\GetEntityParameters.cs + + + EndPoint\DataSync\Entity\PNDataSyncDeleteEntityResult.cs + + + EndPoint\DataSync\Entity\PNDataSyncEntityResult.cs + + + EndPoint\DataSync\Entity\SetEntityOperation.cs + + + EndPoint\DataSync\Entity\SetEntityParameters.cs + + + EndPoint\DataSync\Entity\UpdateEntityOperation.cs + + + EndPoint\DataSync\Entity\UpdateEntityParameters.cs + + + EndPoint\DataSync\Membership\CreateMembershipOperation.cs + + + EndPoint\DataSync\Membership\CreateMembershipParameters.cs + + + EndPoint\DataSync\Membership\DeleteMembershipOperation.cs + + + EndPoint\DataSync\Membership\DeleteMembershipParameters.cs + + + EndPoint\DataSync\Membership\GetMembershipOperation.cs + + + EndPoint\DataSync\Membership\GetMembershipParameters.cs + + + EndPoint\DataSync\Membership\GetMembershipsOperation.cs + + + EndPoint\DataSync\Membership\GetMembershipsParameters.cs + + + EndPoint\DataSync\Membership\PNDataSyncDeleteMembershipResult.cs + + + EndPoint\DataSync\Membership\PNDataSyncMembershipResult.cs + + + EndPoint\DataSync\Membership\SetMembershipOperation.cs + + + EndPoint\DataSync\Membership\SetMembershipParameters.cs + + + EndPoint\DataSync\Membership\UpdateMembershipOperation.cs + + + EndPoint\DataSync\Membership\UpdateMembershipParameters.cs + + + EndPoint\DataSync\Relationship\CreateRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\CreateRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\DeleteRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\DeleteRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\GetRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\GetRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\GetRelationshipsOperation.cs + + + EndPoint\DataSync\Relationship\GetRelationshipsParameters.cs + + + EndPoint\DataSync\Relationship\PNDataSyncDeleteRelationshipResult.cs + + + EndPoint\DataSync\Relationship\PNDataSyncRelationshipResult.cs + + + EndPoint\DataSync\Relationship\SetRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\SetRelationshipParameters.cs + + + EndPoint\DataSync\Relationship\UpdateRelationshipOperation.cs + + + EndPoint\DataSync\Relationship\UpdateRelationshipParameters.cs + + + EndPoint\DataSync\User\CreateUserOperation.cs + + + EndPoint\DataSync\User\CreateUserParameters.cs + + + EndPoint\DataSync\User\DeleteUserOperation.cs + + + EndPoint\DataSync\User\DeleteUserParameters.cs + + + EndPoint\DataSync\User\GetUserOperation.cs + + + EndPoint\DataSync\User\GetUserParameters.cs + + + EndPoint\DataSync\User\GetUsersOperation.cs + + + EndPoint\DataSync\User\GetUsersParameters.cs + + + EndPoint\DataSync\User\PNDataSyncDeleteUserResult.cs + + + EndPoint\DataSync\User\PNDataSyncUserResult.cs + + + EndPoint\DataSync\User\SetUserOperation.cs + + + EndPoint\DataSync\User\SetUserParameters.cs + + + EndPoint\DataSync\User\UpdateUserOperation.cs + + + EndPoint\DataSync\User\UpdateUserParameters.cs + @@ -422,6 +633,39 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + JsonDataParse\PNDataSyncChannelResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncChannelsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEntitiesListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEntityResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncEventJsonDataParse.cs + + + JsonDataParse\PNDataSyncMembershipResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncMembershipsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncRelationshipResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncRelationshipsListResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncUserResultJsonDataParse.cs + + + JsonDataParse\PNDataSyncUsersListResultJsonDataParse.cs + @@ -464,6 +708,12 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea + + Model\Consumer\AccessManager\PNDataSyncProjections.cs + + + Model\Consumer\AccessManager\PNDataSyncTokenScopes.cs + @@ -551,6 +801,9 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea Model\Consumer\Presence\PNWhereNowResult.cs + + Model\Consumer\Pubsub\PNDataSyncEventResult.cs + @@ -604,6 +857,51 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea Model\Derived\ChannelGroup\PNChannelGroupsRemoveChannelResultExt.cs + + Model\Derived\DataSync\PNDataSyncChannelResultExt.cs + + + Model\Derived\DataSync\PNDataSyncChannelsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteChannelResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteEntityResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteMembershipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteRelationshipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncDeleteUserResultExt.cs + + + Model\Derived\DataSync\PNDataSyncEntitiesListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncEntityResultExt.cs + + + Model\Derived\DataSync\PNDataSyncMembershipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncMembershipsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncRelationshipResultExt.cs + + + Model\Derived\DataSync\PNDataSyncRelationshipsListResultExt.cs + + + Model\Derived\DataSync\PNDataSyncUserResultExt.cs + + + Model\Derived\DataSync\PNDataSyncUsersListResultExt.cs + @@ -776,6 +1074,7 @@ Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instea +