Skip to content

Commit

Permalink
Add dataModelName parameter to GetModelMetadata endpoint (#12826)
Browse files Browse the repository at this point in the history
* Make route 'model-metadata' take optional parameter dataModelName

* Add tests for dataModelName param

* Update path in paths.js

* Refactor arrange part of tests into a separate method

* Add 'undefined' value for dataModelName where it is used in frontend code

* Modify GetModelName method and delete unused code

* Add fallback to ApplicationMetadata in GetModelName

* Move layoutSetsMocks into a new file
  • Loading branch information
ErlingHauan authored and Jondyr committed Jun 10, 2024
1 parent a43ee57 commit 53f33b1
Show file tree
Hide file tree
Showing 104 changed files with 321 additions and 257 deletions.
6 changes: 4 additions & 2 deletions backend/src/Designer/Controllers/AppDevelopmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,17 @@ public async Task<IActionResult> GetAppMetadataDataModelIds(string org, string a
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="layoutSetName">Name of current layoutSet in ux-editor that edited layout belongs to</param>
/// <param name="dataModelName">Name of data model to fetch</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The model as JSON</returns>
[HttpGet]
[UseSystemTextJson]
[Route("model-metadata")]
public async Task<IActionResult> GetModelMetadata(string org, string app, [FromQuery] string layoutSetName, CancellationToken cancellationToken)
public async Task<IActionResult> GetModelMetadata(string org, string app, [FromQuery] string layoutSetName, [FromQuery] string dataModelName, CancellationToken cancellationToken)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
ModelMetadata modelMetadata = await _appDevelopmentService.GetModelMetadata(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), layoutSetName, cancellationToken);
ModelMetadata modelMetadata = await _appDevelopmentService.GetModelMetadata(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), layoutSetName, dataModelName, cancellationToken);

return Ok(modelMetadata);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using NuGet.Versioning;
using LayoutSets = Altinn.Studio.Designer.Models.LayoutSets;
using PlatformStorageModels = Altinn.Platform.Storage.Interface.Models;

namespace Altinn.Studio.Designer.Services.Implementation
{
Expand Down Expand Up @@ -190,39 +188,45 @@ public async Task<IEnumerable<string>> GetAppMetadataModelIds(AltinnRepoEditingC
}

/// <inheritdoc />
public async Task<ModelMetadata> GetModelMetadata(AltinnRepoEditingContext altinnRepoEditingContext,
string layoutSetName, CancellationToken cancellationToken = default)
public async Task<ModelMetadata> GetModelMetadata(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetName, string dataModelName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
AltinnAppGitRepository altinnAppGitRepository =
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
ApplicationMetadata applicationMetadata =
await altinnAppGitRepository.GetApplicationMetadata(cancellationToken);
// get task_id since we might not maintain dataType ref in layout-sets-file
string taskId = await GetTaskIdBasedOnLayoutSet(altinnRepoEditingContext, layoutSetName, cancellationToken);
string modelName = GetModelName(applicationMetadata, taskId);
string modelPath;
ModelMetadata modelMetadata;

if (dataModelName is not null)
{
modelPath = $"App/models/{dataModelName}.schema.json";
modelMetadata = await _schemaModelService.GenerateModelMetadataFromJsonSchema(altinnRepoEditingContext, modelPath, cancellationToken);
return modelMetadata;
}

string modelName = await GetModelName(altinnRepoEditingContext, layoutSetName, cancellationToken);

if (string.IsNullOrEmpty(modelName))
{
return new ModelMetadata();
}
string modelPath = $"App/models/{modelName}.schema.json";
ModelMetadata modelMetadata = await _schemaModelService.GenerateModelMetadataFromJsonSchema(altinnRepoEditingContext, modelPath, cancellationToken);

modelPath = $"App/models/{modelName}.schema.json";
modelMetadata = await _schemaModelService.GenerateModelMetadataFromJsonSchema(altinnRepoEditingContext, modelPath, cancellationToken);
return modelMetadata;
}

private string GetModelName(ApplicationMetadata applicationMetadata, [CanBeNull] string taskId)
private async Task<string> GetModelName(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetName, CancellationToken cancellationToken = default)
{
// fallback to first model if no task_id is provided (no layout sets)
if (taskId == null)
if (string.IsNullOrEmpty(layoutSetName))
{
// Fallback to first model in app metadata if no layout set is provided
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
ApplicationMetadata applicationMetadata = await altinnAppGitRepository.GetApplicationMetadata(cancellationToken);
return applicationMetadata.DataTypes.FirstOrDefault(data => data.AppLogic != null && !string.IsNullOrEmpty(data.AppLogic.ClassRef) && !string.IsNullOrEmpty(data.TaskId))?.Id ?? string.Empty;
}

PlatformStorageModels.DataType data = applicationMetadata.DataTypes
.FirstOrDefault(data => data.AppLogic != null && DoesDataTaskMatchTaskId(data, taskId) && !string.IsNullOrEmpty(data.AppLogic.ClassRef));
LayoutSets layoutSets = await GetLayoutSets(altinnRepoEditingContext, cancellationToken);
var foundLayoutSet = layoutSets.Sets.Find(set => set.Id == layoutSetName);

return data?.Id ?? string.Empty;
return foundLayoutSet.DataType;
}

private IEnumerable<string> GetAppMetadataModelIds(ApplicationMetadata applicationMetadata, bool onlyUnReferenced)
Expand All @@ -240,23 +244,6 @@ private IEnumerable<string> GetAppMetadataModelIds(ApplicationMetadata applicati
return appMetaDataDataTypes.Select(datatype => datatype.Id);
}

private bool DoesDataTaskMatchTaskId(PlatformStorageModels.DataType data, [CanBeNull] string taskId)
{
return string.IsNullOrEmpty(taskId) || data.TaskId == taskId;
}

private async Task<string> GetTaskIdBasedOnLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetName, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(layoutSetName))
{
// App without layout sets --> no need for task_id, we just retrieve the first occurence of a dataType with a classRef
return null;
}
LayoutSets layoutSets = await GetLayoutSets(altinnRepoEditingContext, cancellationToken);

return layoutSets?.Sets?.Find(set => set.Id == layoutSetName)?.Tasks[0];
}

/// <inheritdoc />
public async Task<LayoutSets> GetLayoutSets(AltinnRepoEditingContext altinnRepoEditingContext,
CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ public Task<IEnumerable<string>> GetAppMetadataModelIds(
/// </summary>
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
/// <param name="layoutSetName">Name of layoutSet to fetch corresponding model metadata for</param>
/// <param name="dataModelName">Name of data model to fetch</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The model metadata for a given layout set.</returns>
public Task<ModelMetadata> GetModelMetadata(
AltinnRepoEditingContext altinnRepoEditingContext, [CanBeNull] string layoutSetName,
AltinnRepoEditingContext altinnRepoEditingContext, [CanBeNull] string layoutSetName, [CanBeNull] string dataModelName,
CancellationToken cancellationToken = default);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,57 @@ public GetModelMetadataTests(WebApplicationFactory<Program> factory) : base(fact
}

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet1", "TestData/Model/Metadata/datamodel.json")]
[InlineData("ttd", "app-without-layoutsets", "testUser", null, "TestData/Model/Metadata/datamodel.json")]
public async Task GetModelMetadata_Should_Return_ModelMetadata(string org, string app, string developer, string layoutSetName, string expectedModelMetadataPath)
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet1", null)]
[InlineData("ttd", "app-without-layoutsets", "testUser", null, null)]
public async Task GetModelMetadata_Should_Return_ModelMetadata_Based_On_LayoutSet_When_DataModelName_Is_Undefined(string org, string app, string developer, string layoutSetName, string dataModelName)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

string expectedModelMetadata = await AddModelMetadataToRepo(TestRepoPath, expectedModelMetadataPath);

string url = $"{VersionPrefix(org, targetRepository)}/model-metadata?layoutSetName={layoutSetName}";
// Arrange
(string url, string expectedModelMetadata) = await ArrangeGetModelMetadataTest(org, app, developer, layoutSetName, dataModelName);
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

// Act
using var response = await HttpClient.SendAsync(httpRequestMessage);
string responseContent = await response.Content.ReadAsStringAsync();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
responseContent.Should().Be(expectedModelMetadata);
JsonUtils.DeepEquals(expectedModelMetadata, responseContent).Should().BeTrue();
}

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet1", "datamodel")]
[InlineData("ttd", "app-without-layoutsets", "testUser", null, "datamodel")]
public async Task GetModelMetadata_Should_Return_ModelMetadata_When_DataModelName_Is_Specified(string org, string app, string developer, string layoutSetName, string dataModelName)
{
// Arrange
(string url, string expectedModelMetadata) = await ArrangeGetModelMetadataTest(org, app, developer, layoutSetName, dataModelName);
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

// Act
using var response = await HttpClient.SendAsync(httpRequestMessage);
string responseContent = await response.Content.ReadAsStringAsync();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
responseContent.Should().Be(expectedModelMetadata);
JsonUtils.DeepEquals(expectedModelMetadata, responseContent).Should().BeTrue();
}

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet3")]
[InlineData("ttd", "app-without-layoutsets-mismatch-modelname", "testUser", null)]
public async Task GetModelMetadata_Should_Return_404_When_No_Corresponding_Datamodel_Exists(string org, string app, string developer, string layoutSetName)
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet3", null)]
[InlineData("ttd", "app-without-layoutsets-mismatch-modelname", "testUser", null, null)]
[InlineData("ttd", "app-with-layoutsets", "testUser", null, "non-existing-dataModelName")]
public async Task GetModelMetadata_Should_Return_404_When_No_Corresponding_Datamodel_Exists(string org, string app, string developer, string layoutSetName, string dataModelName)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

string url = $"{VersionPrefix(org, targetRepository)}/model-metadata?layoutSetName={layoutSetName}";
// Arrange
(string url, _) = await ArrangeGetModelMetadataTest(org, app, developer, layoutSetName, dataModelName);
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

// Act
using var response = await HttpClient.SendAsync(httpRequestMessage);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

Expand All @@ -63,5 +81,18 @@ private async Task<string> AddModelMetadataToRepo(string createdFolderPath, stri
await File.WriteAllTextAsync(filePath, modelMetadata);
return modelMetadata;
}

private async Task<(string url, string expectedModelMetadata)> ArrangeGetModelMetadataTest(string org, string app, string developer, string layoutSetName, string dataModelName)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

const string expectedModelMetadataPath = "TestData/Model/Metadata/datamodel.json";
string expectedModelMetadata = await AddModelMetadataToRepo(TestRepoPath, expectedModelMetadataPath);

string url = $"{VersionPrefix(org, targetRepository)}/model-metadata?layoutSetName={layoutSetName}&dataModelName={dataModelName}";

return (url, expectedModelMetadata);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { queryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithProviders } from '@altinn/ux-editor/testing/mocks';
import { layoutSet1NameMock, layoutSetsMock } from '@altinn/ux-editor/testing/layoutMock';
import { layoutSet1NameMock, layoutSetsMock } from '@altinn/ux-editor/testing/layoutSetsMock';
import type { AppPreviewSubMenuProps } from './AppPreviewSubMenu';
import { AppPreviewSubMenu } from './AppPreviewSubMenu';
import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const widgetSettingsPath = (org, app) => `${basePath}/${org}/${app}/app-d
export const optionListIdsPath = (org, app) => `${basePath}/${org}/${app}/app-development/option-list-ids`; // Get
export const ruleConfigPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/rule-config?${s({ layoutSetName })}`; // Get, Post
export const appMetadataModelIdsPath = (org, app, onlyUnReferenced) => `${basePath}/${org}/${app}/app-development/model-ids?${s({ onlyUnReferenced })}`; // Get
export const datamodelMetadataPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/model-metadata?${s({ layoutSetName })}`; // Get
export const datamodelMetadataPath = (org, app, layoutSetName, dataModelName) => `${basePath}/${org}/${app}/app-development/model-metadata?${s({ layoutSetName })}&${s({ dataModelName })}`; // Get
export const layoutNamesPath = (org, app) => `${basePath}/${org}/${app}/app-development/layout-names`; // Get
export const layoutSetsPath = (org, app) => `${basePath}/${org}/${app}/app-development/layout-sets`; // Get
export const layoutSetPath = (org, app, layoutSetIdToUpdate) => `${basePath}/${org}/${app}/app-development/layout-set/${layoutSetIdToUpdate}`; // Put, Delete
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const getAppReleases = (owner: string, app: string) => get<AppReleasesRes
export const getAppVersion = (org: string, app: string) => get<AppVersion>(appVersionPath(org, app));
export const getBranchStatus = (owner: string, app: string, branch: string) => get<BranchStatus>(branchStatusPath(owner, app, branch));
export const getDatamodel = (owner: string, app: string, modelPath: string) => get<JsonSchema>(datamodelPath(owner, app, modelPath));
export const getDatamodelMetadata = (owner: string, app: string, layoutSetName: string) => get<DatamodelMetadataResponse>(datamodelMetadataPath(owner, app, layoutSetName));
export const getDatamodelMetadata = (owner: string, app: string, layoutSetName: string, dataModelName: string) => get<DatamodelMetadataResponse>(datamodelMetadataPath(owner, app, layoutSetName, dataModelName));
export const getDatamodelsJson = (owner: string, app: string) => get<DatamodelMetadataJson[]>(datamodelsPath(owner, app));
export const getDatamodelsXsd = (owner: string, app: string) => get<DatamodelMetadataXsd[]>(datamodelsXsdPath(owner, app));
export const getDeployPermissions = (owner: string, app: string) => get<string[]>(deployPermissionsPath(owner, app));
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/ux-editor-v3/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { appStateMock } from './testing/stateMocks';
import type { AppContextProps } from './AppContext';
import ruleHandlerMock from './testing/ruleHandlerMock';
import { layoutSetsMock } from './testing/layoutMock';
import { layoutSetsMock } from './testing/layoutSetsMock';

const { selectedLayoutSet } = appStateMock.formDesigner.layout;

Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/ux-editor-v3/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function App() {
const { data: layoutSets, isSuccess: areLayoutSetsFetched } = useLayoutSetsQuery(org, app);
const { isSuccess: areWidgetsFetched, isError: widgetFetchedError } = useWidgetsQuery(org, app);
const { isSuccess: isDatamodelFetched, isError: dataModelFetchedError } =
useDatamodelMetadataQuery(org, app, selectedLayoutSet);
useDatamodelMetadataQuery(org, app, selectedLayoutSet, undefined);
const { isSuccess: areTextResourcesFetched } = useTextResourcesQuery(org, app);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import userEvent from '@testing-library/user-event';
import { LayoutSetsContainer } from './LayoutSetsContainer';
import { queryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithMockStore } from '../../testing/mocks';
import { layoutSet1NameMock, layoutSet2NameMock, layoutSetsMock } from '../../testing/layoutMock';
import {
layoutSet1NameMock,
layoutSet2NameMock,
layoutSetsMock,
} from '@altinn/ux-editor-v3/testing/layoutSetsMock';
import type { AppContextProps } from '../../AppContext';
import { appStateMock } from '../../testing/stateMocks';
import { QueryKey } from 'app-shared/types/QueryKey';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { mockUseTranslation } from '@studio/testing/mocks/i18nMock';
import { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3';
import { useDatamodelMetadataQuery } from '../../hooks/queries/useDatamodelMetadataQuery';
import type { DatamodelMetadataResponse } from 'app-shared/types/api';
import { layoutSet1NameMock } from '@altinn/ux-editor-v3/testing/layoutMock';
import { dataModelNameMock, layoutSet1NameMock } from '@altinn/ux-editor-v3/testing/layoutSetsMock';
import { app, org } from '@studio/testing/testids';

const user = userEvent.setup();
Expand Down Expand Up @@ -226,7 +226,8 @@ const waitForData = async () => {
const dataModelMetadataResult = renderHookWithMockStore(
{},
{ getDatamodelMetadata },
)(() => useDatamodelMetadataQuery(org, app, layoutSet1NameMock)).renderHookResult.result;
)(() => useDatamodelMetadataQuery(org, app, layoutSet1NameMock, dataModelNameMock))
.renderHookResult.result;
await waitFor(() => expect(dataModelMetadataResult.current.isSuccess).toBe(true));
await waitFor(() => expect(layoutSchemaResult.current[0].isSuccess).toBe(true));
};
Expand Down
Loading

0 comments on commit 53f33b1

Please sign in to comment.