diff --git a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk.Tests/RepositoryTests/GraphSourceRepositoryTests.cs b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk.Tests/RepositoryTests/GraphSourceRepositoryTests.cs index aedae0b..1929537 100644 --- a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk.Tests/RepositoryTests/GraphSourceRepositoryTests.cs +++ b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk.Tests/RepositoryTests/GraphSourceRepositoryTests.cs @@ -169,6 +169,80 @@ public async Task SaveTypesAsync_WithLink_ShouldGenerateTypesAndLink() mockRestClient.VerifyAll(); } + [TestMethod] + public async Task UpdateTypesAsync_BuildsExpectedJsonString_AndCallsGraphClient() + { + // Arrange + repository.ConfigureContentType() + .Field(x => x.FirstName, IndexingType.Searchable) + .Field(x => x.LastName, IndexingType.Searchable) + .Field(x => x.Age, IndexingType.Queryable); + + var expectedJsonString = BuildExpectedTypeJsonString(); + var content = new StringContent(expectedJsonString, Encoding.UTF8, "application/json"); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + var request = new HttpRequestMessage(HttpMethod.Post, $"/api/content/v3/types?id={source}") { Content = content }; + + mockRestClient.Setup(c => c.SendAsync(It.IsAny())).ReturnsAsync(response); + mockRestClient.Setup(c => c.HandleResponse(response)); + + // Act + await repository.UpdateTypesAsync(); + + // Assert + mockRestClient.Verify(c => c.SendAsync(It.Is(x => Compare(request, x))), Times.Once); + mockRestClient.Verify(c => c.HandleResponse(response), Times.Once); + mockRestClient.VerifyAll(); + } + + [TestMethod] + public async Task UpdateTypesAsync_WithLink_ShouldGenerateTypesAndLink() + { + // Arrange + repository.ConfigureContentType() + .Field(x => x.Longitude, IndexingType.Queryable) + .Field(x => x.Latitude, IndexingType.Queryable) + .Field(x => x.Name, IndexingType.Searchable); + + repository.ConfigureContentType() + .Field(x => x.LocationName, IndexingType.Queryable) + .Field(x => x.Time, IndexingType.Queryable) + .Field(x => x.Name, IndexingType.Searchable) + .Field(x => x.AdditionalInfo, IndexingType.PropertyType); + + repository.ConfigureContentType() + .Field(x => x.Example1, IndexingType.OnlyStored) + .Field(x => x.Example2, IndexingType.Queryable); + + repository.ConfigureLink( + "NameToLocationName", + x => x.Name, + x => x.LocationName + ); + + var expectedJsonString = @"{""useTypedFieldNames"":true,""languages"":[],""links"":{""NameToLocationName"":{""from"":""Name$$String___searchable"",""to"":""LocationName$$String""}},""contentTypes"":{""Location"":{""contentType"":[],""properties"":{""Longitude"":{""type"":""Float"",""searchable"":false,""skip"":false},""Latitude"":{""type"":""Float"",""searchable"":false,""skip"":false},""Name"":{""type"":""String"",""searchable"":true,""skip"":false}}},""Event"":{""contentType"":[],""properties"":{""LocationName"":{""type"":""String"",""searchable"":false,""skip"":false},""Time"":{""type"":""DateTime"",""searchable"":false,""skip"":false},""Name"":{""type"":""String"",""searchable"":true,""skip"":false},""AdditionalInfo"":{""type"":""ExtraInfo""}}},""ExtraInfo"":{""contentType"":[],""properties"":{""Example1"":{""type"":""String"",""searchable"":false,""skip"":true},""Example2"":{""type"":""Int"",""searchable"":false,""skip"":false}}}},""propertyTypes"":{}}"; + + var jsonString = BuildExpectedTypeJsonString(); + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + var request = new HttpRequestMessage(HttpMethod.Post, $"/api/content/v3/types?id={source}") { Content = content }; + + mockRestClient.Setup(c => c.SendAsync(It.IsAny())).ReturnsAsync(response); + mockRestClient.Setup(c => c.HandleResponse(response)); + + // Act + await repository.UpdateTypesAsync(); + + // Assert + Assert.AreEqual(expectedJsonString, jsonString); + + mockRestClient.Verify(c => c.SendAsync(It.Is(x => Compare(request, x))), Times.Once); + mockRestClient.Verify(c => c.HandleResponse(response), Times.Once); + mockRestClient.VerifyAll(); + } + [TestMethod] public async Task SaveContentAsync_SerializesData_AndCallsGraphClient() { @@ -296,7 +370,7 @@ public async Task SaveContentAsync_WithMultipleTypes_ShouldGenerateJsonForConten var event1 = new Event { Name = "Future of Project Management", - Time = new DateTime(2024, 10, 22), + Time = new DateTime(2024, 10, 22, 0, 0, 0, DateTimeKind.Utc), LocationName = "Stockholm", AdditionalInfo = new ExtraInfo { @@ -307,7 +381,7 @@ public async Task SaveContentAsync_WithMultipleTypes_ShouldGenerateJsonForConten var event2 = new Event { Name = "Week of Hope: Football Camp for Homeless Children in Hanoi!", - Time = new DateTime(2024, 10, 27), + Time = new DateTime(2024, 10, 27, 0, 0, 0, DateTimeKind.Utc), LocationName = "Hanoi", AdditionalInfo = new ExtraInfo { @@ -318,7 +392,7 @@ public async Task SaveContentAsync_WithMultipleTypes_ShouldGenerateJsonForConten var event3 = new Event { Name = "Optimizing Project Management: Strategies for Success", - Time = new DateTime(2024, 11, 03), + Time = new DateTime(2024, 11, 03, 0, 0, 0, DateTimeKind.Utc), LocationName = "London", AdditionalInfo = new ExtraInfo { @@ -332,11 +406,11 @@ public async Task SaveContentAsync_WithMultipleTypes_ShouldGenerateJsonForConten {""index"":{""_id"":""Location-London"",""language_routing"":""en""}} {""Status$$String"":""Published"",""__typename"":""Location"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Location""],""Language"":{""Name$$String"":""en""},""Longitude$$Float"":0.1275,""Latitude$$Float"":51.5072,""Name$$String___searchable"":""London""} {""index"":{""_id"":""Event-Future of Project Management"",""language_routing"":""en""}} -{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""Stockholm"",""Time$$DateTime"":""2024-10-22T04:00:00Z"",""Name$$String___searchable"":""Future of Project Management"",""AdditionalInfo"":{""Example1$$String___skip"":""test1"",""Example2$$Int"":1}} +{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""Stockholm"",""Time$$DateTime"":""2024-10-22T00:00:00Z"",""Name$$String___searchable"":""Future of Project Management"",""AdditionalInfo"":{""Example1$$String___skip"":""test1"",""Example2$$Int"":1}} {""index"":{""_id"":""Event-Week of Hope: Football Camp for Homeless Children in Hanoi!"",""language_routing"":""en""}} -{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""Hanoi"",""Time$$DateTime"":""2024-10-27T04:00:00Z"",""Name$$String___searchable"":""Week of Hope: Football Camp for Homeless Children in Hanoi!"",""AdditionalInfo"":{""Example1$$String___skip"":""test2"",""Example2$$Int"":2}} +{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""Hanoi"",""Time$$DateTime"":""2024-10-27T00:00:00Z"",""Name$$String___searchable"":""Week of Hope: Football Camp for Homeless Children in Hanoi!"",""AdditionalInfo"":{""Example1$$String___skip"":""test2"",""Example2$$Int"":2}} {""index"":{""_id"":""Event-Optimizing Project Management: Strategies for Success"",""language_routing"":""en""}} -{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""London"",""Time$$DateTime"":""2024-11-03T04:00:00Z"",""Name$$String___searchable"":""Optimizing Project Management: Strategies for Success"",""AdditionalInfo"":{""Example1$$String___skip"":""test3"",""Example2$$Int"":3}} +{""Status$$String"":""Published"",""__typename"":""Event"",""_rbac"":""r:Everyone:Read"",""ContentType$$String"":[""Event""],""Language"":{""Name$$String"":""en""},""LocationName$$String"":""London"",""Time$$DateTime"":""2024-11-03T00:00:00Z"",""Name$$String___searchable"":""Optimizing Project Management: Strategies for Success"",""AdditionalInfo"":{""Example1$$String___skip"":""test3"",""Example2$$Int"":3}} "; Func generateId = (x) => diff --git a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/GraphSourceClient.cs b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/GraphSourceClient.cs index 488f83a..8b81805 100644 --- a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/GraphSourceClient.cs +++ b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/GraphSourceClient.cs @@ -82,6 +82,15 @@ public async Task SaveTypesAsync() return await repository.SaveTypesAsync(); } + /// + /// Updates Content Types set in the SourceConfigurationModel to the Content Graph api. + /// + /// + public async Task UpdateTypesAsync() + { + return await repository.UpdateTypesAsync(); + } + /// /// Saves dynamic content sent in data array to the Content Graph api. /// diff --git a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/Repositories/GraphSourceRepository.cs b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/Repositories/GraphSourceRepository.cs index 3e8ab16..3a04289 100644 --- a/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/Repositories/GraphSourceRepository.cs +++ b/Optimizely.Graph.Source.Sdk/Optimizely.Graph.Source.Sdk/Repositories/GraphSourceRepository.cs @@ -1,175 +1,203 @@ -using Optimizely.Graph.Source.Sdk.JsonConverters; -using Optimizely.Graph.Source.Sdk.RestClientHelpers; -using Optimizely.Graph.Source.Sdk.SourceConfiguration; -using System.Linq.Expressions; -using System.Text; -using System.Text.Json; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace Optimizely.Graph.Source.Sdk.Repositories -{ - /// - /// The GraphSourceRepository manages and delivers content types and content - /// to the Content Graph services api. - /// - public class GraphSourceRepository : IGraphSourceRepository - { - private readonly IRestClient client; - private readonly string source; - - private const string TypeUrl = "/api/content/v3/types"; - private const string DataUrl = "/api/content/v2/data"; - - /// - /// Constructor - /// - /// Rest client targeting Content Graph api services. - /// Content Graph source. - /// - public GraphSourceRepository(IRestClient client, string source) - { - this.client = client; - this.source = source ?? throw new ArgumentNullException(nameof(source)); - } - - /// - public void AddLanguage(string language) - { - SourceConfigurationModel.AddLanguage(language); - } - - /// - public SourceConfigurationModel ConfigureContentType() - where T : class, new() - { - return new SourceConfigurationModel(ConfigurationType.ContentType); - } - - /// - public SourceConfigurationModel ConfigurePropertyType() - where T : class, new() - { - return new SourceConfigurationModel(ConfigurationType.PropertyType); - } - - /// - public async Task SaveTypesAsync() - { - var serializeOptions = new JsonSerializerOptions - { - WriteIndented = true, - Converters = - { - new SourceSdkContentTypeConverter() - } - }; - - var jsonString = JsonSerializer.Serialize(SourceConfigurationModel.GetTypeFieldConfiguration(), serializeOptions); - - var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); - - using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, $"{TypeUrl}?id={source}")) - { - requestMessage.Content = content; - using (var responseMessage = await client.SendAsync(requestMessage)) - { - await client.HandleResponse(responseMessage); - } - } - - return string.Empty; - } - - /// - public async Task SaveContentAsync(Func generateId, string language, params T[] data) - where T : class, new() - { - var content = CreateContent(generateId, language, data); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{DataUrl}?id={source}"); - requestMessage.Content = content; - using var responseMessage = await client.SendAsync(requestMessage); - var response = await client.HandleResponse(responseMessage); - return response?.JournalId ?? string.Empty; - } - - public StringContent CreateContent(Func generateId, string language, params T[] data) - { - var serializeOptions = new JsonSerializerOptions - { - WriteIndented = false, - Converters = - { - new SourceSdkContentConverter() - } - }; - - var itemJson = string.Empty; - foreach (var item in data) - { - var id = generateId(item); - - itemJson += $"{{\"index\":{{\"_id\":\"{id}\",\"language_routing\":\"{language}\"}}}}"; - itemJson += Environment.NewLine; - itemJson += JsonSerializer.Serialize(item, serializeOptions); - itemJson += Environment.NewLine; - } - - var content = new StringContent(itemJson, Encoding.UTF8, "application/json"); - return content; - } - - /// - public async Task DeleteContentAsync() - { - using (var requestMessage = new HttpRequestMessage(HttpMethod.Delete, $"{DataUrl}?id={source}")) - { - using (var responseMessage = await client.SendAsync(requestMessage)) - { - await client.HandleResponse(responseMessage); - } - } - return string.Empty; - } - - public void ConfigureLink(string name, Expression> from, Expression> to) - { - SourceConfigurationModel.ConfigureLink(name, from, to); +using Optimizely.Graph.Source.Sdk.JsonConverters; +using Optimizely.Graph.Source.Sdk.RestClientHelpers; +using Optimizely.Graph.Source.Sdk.SourceConfiguration; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Optimizely.Graph.Source.Sdk.Repositories +{ + /// + /// The GraphSourceRepository manages and delivers content types and content + /// to the Content Graph services api. + /// + public class GraphSourceRepository : IGraphSourceRepository + { + private readonly IRestClient client; + private readonly string source; + + private const string TypeUrl = "/api/content/v3/types"; + private const string DataUrl = "/api/content/v2/data"; + + /// + /// Constructor + /// + /// Rest client targeting Content Graph api services. + /// Content Graph source. + /// + public GraphSourceRepository(IRestClient client, string source) + { + this.client = client; + this.source = source ?? throw new ArgumentNullException(nameof(source)); + } + + /// + public void AddLanguage(string language) + { + SourceConfigurationModel.AddLanguage(language); + } + + /// + public SourceConfigurationModel ConfigureContentType() + where T : class, new() + { + return new SourceConfigurationModel(ConfigurationType.ContentType); + } + + /// + public SourceConfigurationModel ConfigurePropertyType() + where T : class, new() + { + return new SourceConfigurationModel(ConfigurationType.PropertyType); + } + + /// + public async Task SaveTypesAsync() + { + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Converters = + { + new SourceSdkContentTypeConverter() + } + }; + + var jsonString = JsonSerializer.Serialize(SourceConfigurationModel.GetTypeFieldConfiguration(), serializeOptions); + + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + + using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, $"{TypeUrl}?id={source}")) + { + requestMessage.Content = content; + using (var responseMessage = await client.SendAsync(requestMessage)) + { + await client.HandleResponse(responseMessage); + } + } + + return string.Empty; + } + + /// + public async Task UpdateTypesAsync() + { + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Converters = + { + new SourceSdkContentTypeConverter() + } + }; + + var jsonString = JsonSerializer.Serialize(SourceConfigurationModel.GetTypeFieldConfiguration(), serializeOptions); + + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + + using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{TypeUrl}?id={source}")) + { + requestMessage.Content = content; + using (var responseMessage = await client.SendAsync(requestMessage)) + { + await client.HandleResponse(responseMessage); + } + } + + return string.Empty; + } + + /// + public async Task SaveContentAsync(Func generateId, string language, params T[] data) + where T : class, new() + { + var content = CreateContent(generateId, language, data); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{DataUrl}?id={source}"); + requestMessage.Content = content; + using var responseMessage = await client.SendAsync(requestMessage); + var response = await client.HandleResponse(responseMessage); + return response?.JournalId ?? string.Empty; + } + + public StringContent CreateContent(Func generateId, string language, params T[] data) + { + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = false, + Converters = + { + new SourceSdkContentConverter() + } + }; + + var itemJson = string.Empty; + foreach (var item in data) + { + var id = generateId(item); + + itemJson += $"{{\"index\":{{\"_id\":\"{id}\",\"language_routing\":\"{language}\"}}}}"; + itemJson += Environment.NewLine; + itemJson += JsonSerializer.Serialize(item, serializeOptions); + itemJson += Environment.NewLine; + } + + var content = new StringContent(itemJson, Encoding.UTF8, "application/json"); + return content; + } + + /// + public async Task DeleteContentAsync() + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Delete, $"{DataUrl}?id={source}")) + { + using (var responseMessage = await client.SendAsync(requestMessage)) + { + await client.HandleResponse(responseMessage); + } + } + return string.Empty; + } + + public void ConfigureLink(string name, Expression> from, Expression> to) + { + SourceConfigurationModel.ConfigureLink(name, from, to); } public async Task DeleteContentItemsAsync(string language, params string[] ids) { - var serializeOptions = new JsonSerializerOptions - { - WriteIndented = false, - Converters = - { - new SourceSdkContentConverter() - } + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = false, + Converters = + { + new SourceSdkContentConverter() + } }; - var itemJson = string.Empty; - for(int i = 0;i ConfigurePropertyType() where T : class, new(); /// - /// Saves Content Types set in the SourceConfigurationModel to the Content Graph api. + /// Replaces all Content Types set in the SourceConfigurationModel to the Content Graph api. /// /// Task SaveTypesAsync(); + /// + /// Updates specific Content Types set in the SourceConfigurationModel to the Content Graph api. + /// + /// + Task UpdateTypesAsync(); + /// /// Saves dynamic content sent in data array to the Content Graph api. /// @@ -49,7 +55,7 @@ public interface IGraphSourceRepository Task DeleteContentAsync(); /// - /// Removes content previously stored by source. + /// Removes individual content items previously stored by source. /// /// Task DeleteContentItemsAsync(string language, params string[] ids);