Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add endpoint for checking for option lists usages #14291

Merged
merged 8 commits into from
Dec 30, 2024
20 changes: 20 additions & 0 deletions backend/src/Designer/Controllers/OptionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;
using LibGit2Sharp;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -107,6 +108,25 @@ public async Task<ActionResult<List<Option>>> GetOptionsList(string org, string
}
}

/// <summary>
/// Gets all usages of all optionListIds in the layouts as <see cref="RefToOptionListSpecifier"/>.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Route("usage")]
public async Task<ActionResult<List<RefToOptionListSpecifier>>> GetOptionListsReferences(string org, string repo, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

List<RefToOptionListSpecifier> optionListReferences = await _optionsService.GetAllOptionListReferences(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), cancellationToken);
return Ok(optionListReferences);
}

/// <summary>
/// Creates or overwrites an options list.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.App;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.TypedHttpClients.Exceptions;
using LibGit2Sharp;
using JsonSerializer = System.Text.Json.JsonSerializer;
Expand Down Expand Up @@ -504,6 +505,94 @@ public async Task<JsonNode> GetLayoutSettingsAndCreateNewIfNotFound(string layou
return layoutSettings;
}

/// <summary>
/// Finds all <see cref="RefToOptionListSpecifier"/> in a given layout.
/// </summary>
/// <param name="layout">The layout.</param>
/// <param name="refToOptionListSpecifiers">A list of occurrences to append any optionListIdRefs in the layout to.</param>
/// <param name="layoutSetName">The layoutSetName the layout belongs to.</param>
/// <param name="layoutName">The name of the given layout.</param>
/// <returns>A list of <see cref="RefToOptionListSpecifier"/>.</returns>
public List<RefToOptionListSpecifier> FindOptionListReferencesInLayout(JsonNode layout, List<RefToOptionListSpecifier> refToOptionListSpecifiers, string layoutSetName, string layoutName)
{
var optionListIds = GetOptionsListIds();
var layoutArray = layout["data"]?["layout"] as JsonArray;
if (layoutArray == null)
{
return refToOptionListSpecifiers;
}

foreach (var item in layoutArray)
Fixed Show fixed Hide fixed
{
string optionListId = item["optionsId"]?.ToString();

if (!optionListIds.Contains(optionListId))
{
continue;
}

if (!String.IsNullOrEmpty(optionListId))
{
if (OptionListIdAlreadyOccurred(refToOptionListSpecifiers, optionListId, out var existingRef))
{
if (OptionListIdAlreadyOccurredInLayout(existingRef, layoutSetName, layoutName, out var existingSource))
{
existingSource.ComponentIds.Add(item["id"]?.ToString());
}
else
{
AddNewOptionListIdSource(existingRef, layoutSetName, layoutName, item["id"]?.ToString());
}
}
else
{
AddNewRefToOptionListSpecifier(refToOptionListSpecifiers, optionListId, layoutSetName, layoutName, item["id"]?.ToString());
}
}
}
return refToOptionListSpecifiers;
}

private bool OptionListIdAlreadyOccurred(List<RefToOptionListSpecifier> refToOptionListSpecifiers, string optionListId, out RefToOptionListSpecifier existingRef)
{
existingRef = refToOptionListSpecifiers.FirstOrDefault(refToOptionList => refToOptionList.OptionListId == optionListId);
return existingRef != null;
}

private bool OptionListIdAlreadyOccurredInLayout(RefToOptionListSpecifier refToOptionListSpecifier, string layoutSetName, string layoutName, out OptionListIdSource existingSource)
{
existingSource = refToOptionListSpecifier.OptionListIdSources
.FirstOrDefault(optionListIdSource => optionListIdSource.LayoutSetId == layoutSetName && optionListIdSource.LayoutName == layoutName);
return existingSource != null;
}

private void AddNewRefToOptionListSpecifier(List<RefToOptionListSpecifier> refToOptionListSpecifiers, string optionListId, string layoutSetName, string layoutName, string componentId)
{
refToOptionListSpecifiers.Add(new()
{
OptionListId = optionListId,
OptionListIdSources =
[
new OptionListIdSource
{
LayoutSetId = layoutSetName,
LayoutName = layoutName,
ComponentIds = [componentId]
}
]
});
}

private void AddNewOptionListIdSource(RefToOptionListSpecifier refToOptionListSpecifier, string layoutSetName, string layoutName, string componentId)
{
refToOptionListSpecifier.OptionListIdSources.Add(new OptionListIdSource
{
LayoutSetId = layoutSetName,
LayoutName = layoutName,
ComponentIds = [componentId]
});
}

private async Task CreateLayoutSettings(string layoutSetName)
{
string layoutSetPath = GetPathToLayoutSet(layoutSetName);
Expand Down
22 changes: 22 additions & 0 deletions backend/src/Designer/Models/Dto/OptionListReferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Altinn.Studio.Designer.Models.Dto;

public class RefToOptionListSpecifier
{
[JsonPropertyName("optionListId")]
public string OptionListId { get; set; }
[JsonPropertyName("optionListIdSources")]
public List<OptionListIdSource> OptionListIdSources { get; set; }
}

public class OptionListIdSource
{
[JsonPropertyName("layoutSetId")]
public string LayoutSetId { get; set; }
[JsonPropertyName("layoutName")]
public string LayoutName { get; set; }
[JsonPropertyName("componentIds")]
public List<string> ComponentIds { get; set; }
}
26 changes: 25 additions & 1 deletion backend/src/Designer/Services/Implementation/OptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Altinn.Studio.Designer.Exceptions.Options;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;
using LibGit2Sharp;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -62,10 +63,33 @@ public async Task<List<Option>> GetOptionsList(string org, string repo, string d
throw new InvalidOptionsFormatException($"One or more of the options have an invalid format in option list: {optionsListId}.");
}


return optionsList;
}

/// <inheritdoc />
public async Task<List<RefToOptionListSpecifier>> GetAllOptionListReferences(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
AltinnAppGitRepository altinnAppGitRepository =
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);

List<RefToOptionListSpecifier> optionsListReferences = new List<RefToOptionListSpecifier>();

string[] layoutSetNames = altinnAppGitRepository.GetLayoutSetNames();
foreach (string layoutSetName in layoutSetNames)
{
string[] layoutNames = altinnAppGitRepository.GetLayoutNames(layoutSetName);
foreach (var layoutName in layoutNames)
{
var layout = await altinnAppGitRepository.GetLayout(layoutSetName, layoutName, cancellationToken);
optionsListReferences = altinnAppGitRepository.FindOptionListReferencesInLayout(layout, optionsListReferences, layoutSetName, layoutName);
}
}

return optionsListReferences;
}

ErlingHauan marked this conversation as resolved.
Show resolved Hide resolved
private void ValidateOption(Option option)
{
var validationContext = new ValidationContext(option);
Expand Down
11 changes: 10 additions & 1 deletion backend/src/Designer/Services/Interfaces/IOptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Microsoft.AspNetCore.Http;

namespace Altinn.Studio.Designer.Services.Interfaces;
Expand Down Expand Up @@ -31,6 +32,14 @@ public interface IOptionsService
/// <returns>The options list</returns>
public Task<List<Option>> GetOptionsList(string org, string repo, string developer, string optionsListId, CancellationToken cancellationToken = default);

/// <summary>
/// Gets a list of sources, <see cref="RefToOptionListSpecifier"/>, for all OptionListIds.
/// </summary>
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>A list of <see cref="RefToOptionListSpecifier"/></returns>
public Task<List<RefToOptionListSpecifier>> GetAllOptionListReferences(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default);

/// <summary>
/// Creates a new options list in the app repository.
/// If the file already exists, it will be overwritten.
Expand All @@ -47,7 +56,7 @@ public interface IOptionsService
/// Adds a new option to the option list.
/// If the file already exists, it will be overwritten.
/// </summary>
/// <param name="org">Orginisation</param>
/// <param name="org">Organisation</param>
/// <param name="repo">Repository</param>
/// <param name="developer">Username of developer</param>
/// <param name="optionsListId">Name of the new options list</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.ApiTests;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace Designer.Tests.Controllers.OptionsController;

public class GetOptionListsReferencesTests : DesignerEndpointsTestsBase<GetOptionListsReferencesTests>, IClassFixture<WebApplicationFactory<Program>>
{
const string RepoWithUsedOptions = "app-with-options";
const string RepoWithUnusedOptions = "app-with-layoutsets";

public GetOptionListsReferencesTests(WebApplicationFactory<Program> factory) : base(factory)
{
}

[Fact]
public async Task GetOptionListsReferences_Returns200OK_WithValidOptionsReferences()
{
string apiUrl = $"/designer/api/ttd/{RepoWithUsedOptions}/options/usage";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl);

using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
string responseBody = await response.Content.ReadAsStringAsync();
List<RefToOptionListSpecifier> responseList = JsonSerializer.Deserialize<List<RefToOptionListSpecifier>>(responseBody);

List<RefToOptionListSpecifier> expectedResponseList = new()
{
new RefToOptionListSpecifier
{
OptionListId = "test-options", OptionListIdSources =
[
new OptionListIdSource
{
ComponentIds = ["component-using-same-options-id-in-same-set-and-another-layout"],
LayoutName = "layoutWithOneOptionListIdRef.json",
LayoutSetId = "layoutSet1"
},
new OptionListIdSource
{
ComponentIds = ["component-using-test-options-id", "component-using-test-options-id-again"],
LayoutName = "layoutWithFourCheckboxComponentsAndThreeOptionListIdRefs.json",
LayoutSetId = "layoutSet1"
},
new OptionListIdSource
{
ComponentIds = ["component-using-same-options-id-in-another-set"],
LayoutName = "layoutWithTwoOptionListIdRefs.json",
LayoutSetId = "layoutSet2"
}
]
},
new()
{
OptionListId = "other-options", OptionListIdSources =
[
new OptionListIdSource
{
ComponentIds = ["component-using-other-options-id"],
LayoutName = "layoutWithFourCheckboxComponentsAndThreeOptionListIdRefs.json",
LayoutSetId = "layoutSet1"
}
]
}
};

Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
Assert.Equivalent(expectedResponseList, responseList);
}

[Fact]
public async Task GetOptionListsReferences_Returns200Ok_WithEmptyOptionsReferences_WhenLayoutsDoesNotReferenceAnyOptionsInApp()
{
string apiUrl = $"/designer/api/ttd/{RepoWithUnusedOptions}/options/usage";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl);

using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
string responseBody = await response.Content.ReadAsStringAsync();

Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
Assert.Equivalent("[]", responseBody);
}
}
Loading
Loading