Skip to content

Commit 764f572

Browse files
authored
feat: add endpoint for checking for option lists usages (#14291)
1 parent d631b1e commit 764f572

File tree

10 files changed

+372
-5
lines changed

10 files changed

+372
-5
lines changed

backend/src/Designer/Controllers/OptionsController.cs

+20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using Altinn.Studio.Designer.Helpers;
77
using Altinn.Studio.Designer.Models;
8+
using Altinn.Studio.Designer.Models.Dto;
89
using Altinn.Studio.Designer.Services.Interfaces;
910
using LibGit2Sharp;
1011
using Microsoft.AspNetCore.Authorization;
@@ -107,6 +108,25 @@ public async Task<ActionResult<List<Option>>> GetOptionsList(string org, string
107108
}
108109
}
109110

111+
/// <summary>
112+
/// Gets all usages of all optionListIds in the layouts as <see cref="RefToOptionListSpecifier"/>.
113+
/// </summary>
114+
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
115+
/// <param name="repo">Application identifier which is unique within an organisation.</param>
116+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
117+
[HttpGet]
118+
[Produces("application/json")]
119+
[ProducesResponseType(StatusCodes.Status200OK)]
120+
[Route("usage")]
121+
public async Task<ActionResult<List<RefToOptionListSpecifier>>> GetOptionListsReferences(string org, string repo, CancellationToken cancellationToken = default)
122+
{
123+
cancellationToken.ThrowIfCancellationRequested();
124+
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
125+
126+
List<RefToOptionListSpecifier> optionListReferences = await _optionsService.GetAllOptionListReferences(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), cancellationToken);
127+
return Ok(optionListReferences);
128+
}
129+
110130
/// <summary>
111131
/// Creates or overwrites an options list.
112132
/// </summary>

backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs

+89
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Altinn.Studio.Designer.Helpers;
1616
using Altinn.Studio.Designer.Models;
1717
using Altinn.Studio.Designer.Models.App;
18+
using Altinn.Studio.Designer.Models.Dto;
1819
using Altinn.Studio.Designer.TypedHttpClients.Exceptions;
1920
using LibGit2Sharp;
2021
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -504,6 +505,94 @@ public async Task<JsonNode> GetLayoutSettingsAndCreateNewIfNotFound(string layou
504505
return layoutSettings;
505506
}
506507

508+
/// <summary>
509+
/// Finds all <see cref="RefToOptionListSpecifier"/> in a given layout.
510+
/// </summary>
511+
/// <param name="layout">The layout.</param>
512+
/// <param name="refToOptionListSpecifiers">A list of occurrences to append any optionListIdRefs in the layout to.</param>
513+
/// <param name="layoutSetName">The layoutSetName the layout belongs to.</param>
514+
/// <param name="layoutName">The name of the given layout.</param>
515+
/// <returns>A list of <see cref="RefToOptionListSpecifier"/>.</returns>
516+
public List<RefToOptionListSpecifier> FindOptionListReferencesInLayout(JsonNode layout, List<RefToOptionListSpecifier> refToOptionListSpecifiers, string layoutSetName, string layoutName)
517+
{
518+
var optionListIds = GetOptionsListIds();
519+
var layoutArray = layout["data"]?["layout"] as JsonArray;
520+
if (layoutArray == null)
521+
{
522+
return refToOptionListSpecifiers;
523+
}
524+
525+
foreach (var item in layoutArray)
526+
{
527+
string optionListId = item["optionsId"]?.ToString();
528+
529+
if (!optionListIds.Contains(optionListId))
530+
{
531+
continue;
532+
}
533+
534+
if (!String.IsNullOrEmpty(optionListId))
535+
{
536+
if (OptionListIdAlreadyOccurred(refToOptionListSpecifiers, optionListId, out var existingRef))
537+
{
538+
if (OptionListIdAlreadyOccurredInLayout(existingRef, layoutSetName, layoutName, out var existingSource))
539+
{
540+
existingSource.ComponentIds.Add(item["id"]?.ToString());
541+
}
542+
else
543+
{
544+
AddNewOptionListIdSource(existingRef, layoutSetName, layoutName, item["id"]?.ToString());
545+
}
546+
}
547+
else
548+
{
549+
AddNewRefToOptionListSpecifier(refToOptionListSpecifiers, optionListId, layoutSetName, layoutName, item["id"]?.ToString());
550+
}
551+
}
552+
}
553+
return refToOptionListSpecifiers;
554+
}
555+
556+
private bool OptionListIdAlreadyOccurred(List<RefToOptionListSpecifier> refToOptionListSpecifiers, string optionListId, out RefToOptionListSpecifier existingRef)
557+
{
558+
existingRef = refToOptionListSpecifiers.FirstOrDefault(refToOptionList => refToOptionList.OptionListId == optionListId);
559+
return existingRef != null;
560+
}
561+
562+
private bool OptionListIdAlreadyOccurredInLayout(RefToOptionListSpecifier refToOptionListSpecifier, string layoutSetName, string layoutName, out OptionListIdSource existingSource)
563+
{
564+
existingSource = refToOptionListSpecifier.OptionListIdSources
565+
.FirstOrDefault(optionListIdSource => optionListIdSource.LayoutSetId == layoutSetName && optionListIdSource.LayoutName == layoutName);
566+
return existingSource != null;
567+
}
568+
569+
private void AddNewRefToOptionListSpecifier(List<RefToOptionListSpecifier> refToOptionListSpecifiers, string optionListId, string layoutSetName, string layoutName, string componentId)
570+
{
571+
refToOptionListSpecifiers.Add(new()
572+
{
573+
OptionListId = optionListId,
574+
OptionListIdSources =
575+
[
576+
new OptionListIdSource
577+
{
578+
LayoutSetId = layoutSetName,
579+
LayoutName = layoutName,
580+
ComponentIds = [componentId]
581+
}
582+
]
583+
});
584+
}
585+
586+
private void AddNewOptionListIdSource(RefToOptionListSpecifier refToOptionListSpecifier, string layoutSetName, string layoutName, string componentId)
587+
{
588+
refToOptionListSpecifier.OptionListIdSources.Add(new OptionListIdSource
589+
{
590+
LayoutSetId = layoutSetName,
591+
LayoutName = layoutName,
592+
ComponentIds = [componentId]
593+
});
594+
}
595+
507596
private async Task CreateLayoutSettings(string layoutSetName)
508597
{
509598
string layoutSetPath = GetPathToLayoutSet(layoutSetName);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Altinn.Studio.Designer.Models.Dto;
5+
6+
public class RefToOptionListSpecifier
7+
{
8+
[JsonPropertyName("optionListId")]
9+
public string OptionListId { get; set; }
10+
[JsonPropertyName("optionListIdSources")]
11+
public List<OptionListIdSource> OptionListIdSources { get; set; }
12+
}
13+
14+
public class OptionListIdSource
15+
{
16+
[JsonPropertyName("layoutSetId")]
17+
public string LayoutSetId { get; set; }
18+
[JsonPropertyName("layoutName")]
19+
public string LayoutName { get; set; }
20+
[JsonPropertyName("componentIds")]
21+
public List<string> ComponentIds { get; set; }
22+
}

backend/src/Designer/Services/Implementation/OptionsService.cs

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Altinn.Studio.Designer.Exceptions.Options;
77
using Altinn.Studio.Designer.Infrastructure.GitRepository;
88
using Altinn.Studio.Designer.Models;
9+
using Altinn.Studio.Designer.Models.Dto;
910
using Altinn.Studio.Designer.Services.Interfaces;
1011
using LibGit2Sharp;
1112
using Microsoft.AspNetCore.Http;
@@ -62,10 +63,33 @@ public async Task<List<Option>> GetOptionsList(string org, string repo, string d
6263
throw new InvalidOptionsFormatException($"One or more of the options have an invalid format in option list: {optionsListId}.");
6364
}
6465

65-
6666
return optionsList;
6767
}
6868

69+
/// <inheritdoc />
70+
public async Task<List<RefToOptionListSpecifier>> GetAllOptionListReferences(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default)
71+
{
72+
cancellationToken.ThrowIfCancellationRequested();
73+
AltinnAppGitRepository altinnAppGitRepository =
74+
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
75+
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
76+
77+
List<RefToOptionListSpecifier> optionsListReferences = new List<RefToOptionListSpecifier>();
78+
79+
string[] layoutSetNames = altinnAppGitRepository.GetLayoutSetNames();
80+
foreach (string layoutSetName in layoutSetNames)
81+
{
82+
string[] layoutNames = altinnAppGitRepository.GetLayoutNames(layoutSetName);
83+
foreach (var layoutName in layoutNames)
84+
{
85+
var layout = await altinnAppGitRepository.GetLayout(layoutSetName, layoutName, cancellationToken);
86+
optionsListReferences = altinnAppGitRepository.FindOptionListReferencesInLayout(layout, optionsListReferences, layoutSetName, layoutName);
87+
}
88+
}
89+
90+
return optionsListReferences;
91+
}
92+
6993
private void ValidateOption(Option option)
7094
{
7195
var validationContext = new ValidationContext(option);

backend/src/Designer/Services/Interfaces/IOptionsService.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Threading;
33
using System.Threading.Tasks;
44
using Altinn.Studio.Designer.Models;
5+
using Altinn.Studio.Designer.Models.Dto;
56
using Microsoft.AspNetCore.Http;
67

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

35+
/// <summary>
36+
/// Gets a list of sources, <see cref="RefToOptionListSpecifier"/>, for all OptionListIds.
37+
/// </summary>
38+
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
39+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
40+
/// <returns>A list of <see cref="RefToOptionListSpecifier"/></returns>
41+
public Task<List<RefToOptionListSpecifier>> GetAllOptionListReferences(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default);
42+
3443
/// <summary>
3544
/// Creates a new options list in the app repository.
3645
/// If the file already exists, it will be overwritten.
@@ -47,7 +56,7 @@ public interface IOptionsService
4756
/// Adds a new option to the option list.
4857
/// If the file already exists, it will be overwritten.
4958
/// </summary>
50-
/// <param name="org">Orginisation</param>
59+
/// <param name="org">Organisation</param>
5160
/// <param name="repo">Repository</param>
5261
/// <param name="developer">Username of developer</param>
5362
/// <param name="optionsListId">Name of the new options list</param>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.Collections.Generic;
2+
using System.Net.Http;
3+
using System.Text.Json;
4+
using System.Threading.Tasks;
5+
using Altinn.Studio.Designer.Models.Dto;
6+
using Designer.Tests.Controllers.ApiTests;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc.Testing;
9+
using Xunit;
10+
11+
namespace Designer.Tests.Controllers.OptionsController;
12+
13+
public class GetOptionListsReferencesTests : DesignerEndpointsTestsBase<GetOptionListsReferencesTests>, IClassFixture<WebApplicationFactory<Program>>
14+
{
15+
const string RepoWithUsedOptions = "app-with-options";
16+
const string RepoWithUnusedOptions = "app-with-layoutsets";
17+
18+
public GetOptionListsReferencesTests(WebApplicationFactory<Program> factory) : base(factory)
19+
{
20+
}
21+
22+
[Fact]
23+
public async Task GetOptionListsReferences_Returns200OK_WithValidOptionsReferences()
24+
{
25+
string apiUrl = $"/designer/api/ttd/{RepoWithUsedOptions}/options/usage";
26+
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl);
27+
28+
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
29+
string responseBody = await response.Content.ReadAsStringAsync();
30+
List<RefToOptionListSpecifier> responseList = JsonSerializer.Deserialize<List<RefToOptionListSpecifier>>(responseBody);
31+
32+
List<RefToOptionListSpecifier> expectedResponseList = new()
33+
{
34+
new RefToOptionListSpecifier
35+
{
36+
OptionListId = "test-options", OptionListIdSources =
37+
[
38+
new OptionListIdSource
39+
{
40+
ComponentIds = ["component-using-same-options-id-in-same-set-and-another-layout"],
41+
LayoutName = "layoutWithOneOptionListIdRef.json",
42+
LayoutSetId = "layoutSet1"
43+
},
44+
new OptionListIdSource
45+
{
46+
ComponentIds = ["component-using-test-options-id", "component-using-test-options-id-again"],
47+
LayoutName = "layoutWithFourCheckboxComponentsAndThreeOptionListIdRefs.json",
48+
LayoutSetId = "layoutSet1"
49+
},
50+
new OptionListIdSource
51+
{
52+
ComponentIds = ["component-using-same-options-id-in-another-set"],
53+
LayoutName = "layoutWithTwoOptionListIdRefs.json",
54+
LayoutSetId = "layoutSet2"
55+
}
56+
]
57+
},
58+
new()
59+
{
60+
OptionListId = "other-options", OptionListIdSources =
61+
[
62+
new OptionListIdSource
63+
{
64+
ComponentIds = ["component-using-other-options-id"],
65+
LayoutName = "layoutWithFourCheckboxComponentsAndThreeOptionListIdRefs.json",
66+
LayoutSetId = "layoutSet1"
67+
}
68+
]
69+
}
70+
};
71+
72+
Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
73+
Assert.Equivalent(expectedResponseList, responseList);
74+
}
75+
76+
[Fact]
77+
public async Task GetOptionListsReferences_Returns200Ok_WithEmptyOptionsReferences_WhenLayoutsDoesNotReferenceAnyOptionsInApp()
78+
{
79+
string apiUrl = $"/designer/api/ttd/{RepoWithUnusedOptions}/options/usage";
80+
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl);
81+
82+
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
83+
string responseBody = await response.Content.ReadAsStringAsync();
84+
85+
Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
86+
Assert.Equivalent("[]", responseBody);
87+
}
88+
}

0 commit comments

Comments
 (0)