diff --git a/backend/src/Designer/Controllers/Preview/DataController.cs b/backend/src/Designer/Controllers/Preview/DataController.cs new file mode 100644 index 00000000000..d4515b42cf9 --- /dev/null +++ b/backend/src/Designer/Controllers/Preview/DataController.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Altinn.Platform.Storage.Interface.Models; +using Altinn.Studio.Designer.Filters; +using Altinn.Studio.Designer.Helpers; +using Altinn.Studio.Designer.Models; +using Altinn.Studio.Designer.Models.Preview; +using Altinn.Studio.Designer.Services.Implementation; +using Altinn.Studio.Designer.Services.Interfaces; +using Altinn.Studio.Designer.Services.Interfaces.Preview; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Studio.Designer.Controllers.Preview +{ + [Authorize] + [AutoValidateAntiforgeryToken] + [Route("{org:regex(^(?!designer))}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/instances/{partyId}/{instanceGuid}/data")] + public class DataController(IHttpContextAccessor httpContextAccessor, + IPreviewService previewService, + ISchemaModelService schemaModelService, + IDataService dataService + ) : Controller + { + [HttpGet("{dataGuid}")] + public ActionResult Get([FromRoute] Guid dataGuid) + { + JsonNode dataItem = dataService.GetDataElement(dataGuid); + return Ok(dataItem); + } + + [HttpPost] + public ActionResult Post( + [FromRoute] int partyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string dataType + ) + { + DataElement dataElement = dataService.CreateDataElement(partyId, instanceGuid, dataType); + return Created("link-to-app-placeholder", dataElement); + } + + [HttpPatch("{dataGuid}")] + [UseSystemTextJson] + public ActionResult Patch( + [FromRoute] Guid dataGuid, + [FromBody] DataPatchRequest dataPatch + ) + { + JsonNode dataItem = dataService.PatchDataElement(dataGuid, dataPatch.Patch); + return Ok(new DataPatchResponse() + { + ValidationIssues = [], + NewDataModel = dataItem, + }); + } + + [HttpDelete("{dataTypeId}")] + public ActionResult DeleteAttachment([FromRoute] Guid dataGuid) + { + return Ok(); + } + + [HttpGet("{dataGuid}/validate")] + public ActionResult ValidateInstanceForData([FromRoute] Guid dataGuid) + { + return Ok(new List()); + } + + [HttpPost("{dataTypeId}/tags")] + public ActionResult UpdateTagsForAttachment([FromBody] string tag) + { + return Created("link-to-app-placeholder", tag); + } + + [HttpGet(PreviewService.MockDataTaskId)] + public async Task GetDefaultFormData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int partyId, + CancellationToken cancellationToken + ) + { + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + string refererHeader = Request.Headers.Referer; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + DataType dataType = await previewService.GetDataTypeForLayoutSetName(org, app, developer, layoutSetName, cancellationToken); + // For apps that does not have a datamodel + if (dataType == null) + { + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); + return Ok(mockInstance.Id); + } + string modelPath = $"/App/models/{dataType.Id}.schema.json"; + string decodedPath = Uri.UnescapeDataString(modelPath); + string formData = await schemaModelService.GetSchema(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), decodedPath, cancellationToken); + return Ok(formData); + } + + [HttpPut(PreviewService.MockDataTaskId)] + public async Task UpdateFormData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int partyId, + CancellationToken cancellationToken + ) + { + return await GetDefaultFormData(org, app, partyId, cancellationToken); + } + + [HttpPatch(PreviewService.MockDataTaskId)] + public ActionResult PatchFormData() + { + return Ok(); + } + + private static string GetSelectedLayoutSetInEditorFromRefererHeader(string refererHeader) + { + Uri refererUri = new(refererHeader); + string layoutSetName = HttpUtility.ParseQueryString(refererUri.Query)["selectedLayoutSet"]; + + return string.IsNullOrEmpty(layoutSetName) ? null : layoutSetName; + } + } +} + diff --git a/backend/src/Designer/Controllers/Preview/InstancesController.cs b/backend/src/Designer/Controllers/Preview/InstancesController.cs new file mode 100644 index 00000000000..12bfaa02504 --- /dev/null +++ b/backend/src/Designer/Controllers/Preview/InstancesController.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.Platform.Storage.Interface.Models; +using Altinn.Studio.Designer.Helpers; +using Altinn.Studio.Designer.Infrastructure.GitRepository; +using Altinn.Studio.Designer.Services.Interfaces; +using LibGit2Sharp; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Studio.Designer.Controllers.Preview +{ + [Authorize] + [AutoValidateAntiforgeryToken] + [Route("{org:regex(^(?!designer))}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/instances")] + public class InstancesController(IHttpContextAccessor httpContextAccessor, + IPreviewService previewService, + IAltinnGitRepositoryFactory altinnGitRepositoryFactory + ) : Controller + { + /// + /// Action for creating the mocked instance object + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// + /// A that observes if operation is cancelled. + /// The mocked instance object + [HttpPost] + public async Task> Instances(string org, string app, [FromQuery] int? instanceOwnerPartyId, CancellationToken cancellationToken) + { + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + string refererHeader = Request.Headers["Referer"]; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, instanceOwnerPartyId, layoutSetName, cancellationToken); + return Ok(mockInstance); + } + + /// + /// Action for getting a mocked response for the current task connected to the instance + /// + /// The processState + [HttpGet("{partyId}/{instanceGuId}/process")] + public async Task> Process(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) + { + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + string refererHeader = Request.Headers["Referer"]; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); + List tasks = await previewService.GetTasksForAllLayoutSets(org, app, developer, cancellationToken); + AppProcessState processState = new AppProcessState(mockInstance.Process) + { + ProcessTasks = tasks != null + ? new List(tasks?.ConvertAll(task => new AppProcessTaskTypeInfo { ElementId = task, AltinnTaskType = "data" })) + : null + }; + + return Ok(processState); + } + + /// + /// Endpoint to get instance for next process step + /// + /// A mocked instance object + [HttpGet("{partyId}/{instanceGuId}")] + public async Task> InstanceForNextTask(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) + { + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + string refererHeader = Request.Headers["Referer"]; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); + return Ok(mockInstance); + } + + /// + /// Endpoint to get active instances for apps with state/layout sets/multiple processes + /// + /// A list of a single mocked instance + [HttpGet("{partyId}/active")] + public ActionResult> ActiveInstancesForAppsWithLayoutSets(string org, string app, [FromRoute] int partyId) + { + // Simulate never having any active instances + List activeInstances = new(); + return Ok(activeInstances); + } + + /// + /// Endpoint to validate an instance + /// + /// Ok + [HttpGet("{partyId}/{instanceGuId}/validate")] + public ActionResult ValidateInstance() + { + return Ok(); + } + + /// + /// Action for getting a mocked response for the next task connected to the instance + /// + /// The processState object on the global mockInstance object + [HttpGet("{partyId}/{instanceGuId}/process/next")] + public async Task ProcessNext(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) + { + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + string refererHeader = Request.Headers["Referer"]; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); + return Ok(mockInstance.Process); + } + + /// + /// Action for mocking an end to the process in order to get receipt after "send inn" is pressed + /// + /// Process object where ended is set + [HttpPut("{partyId}/{instanceGuId}/process/next")] + public async Task UpdateProcessNext(string org, string app, [FromRoute] int partyId, [FromQuery] string lang, CancellationToken cancellationToken) + { + string refererHeader = Request.Headers["Referer"]; + string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); + if (string.IsNullOrEmpty(layoutSetName)) + { + string endProcess = """{"ended": "ended"}"""; + return Ok(endProcess); + } + string developer = AuthenticationHelper.GetDeveloperUserName(httpContextAccessor.HttpContext); + Instance mockInstance = await previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); + return Ok(mockInstance.Process); + } + + /// + /// Action for getting options list for a given options list id for a given instance + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The id of the options list + /// The language for the options list + /// The source of the options list + /// A that observes if operation is cancelled. + /// The options list if it exists, otherwise nothing + [HttpGet("{partyId}/{instanceGuid}/options/{optionListId}")] + public async Task> GetOptionsForInstance(string org, string app, string optionListId, [FromQuery] string language, [FromQuery] string source, CancellationToken cancellationToken) + { + try + { + // TODO: Need code to get dynamic options list based on language and source? + string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); + AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer); + string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken); + return Ok(options); + } + catch (NotFoundException) + { + // Return empty list since app-frontend don't handle a null result + return Ok(new List()); + } + } + + /// + /// Action for getting data list for a given data list id for a given instance + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The id of the data list + /// The language for the data list + /// The number of items to return + /// A that observes if operation is cancelled. + /// The options list if it exists, otherwise nothing + [HttpGet("{partyId}/{instanceGuid}/datalists/{dataListId}")] + public ActionResult> GetDataListsForInstance(string org, string app, string dataListId, [FromQuery] string language, [FromQuery] string size, CancellationToken cancellationToken) + { + // TODO: Should look into whether we can get some actual data here, or if we can make an "informed" mock based on the setup. + // For now, we just return an empty list. + return Ok(new List()); + } + + /// + /// Action for updating data model with tag for attachment component // TODO: Figure out what actually happens here + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Current page in running app + /// Current layout set in running app + /// Connected datatype for that process task + /// The options list if it exists, otherwise nothing + [HttpPost("{partyId}/{instanceGuid}/pages/order")] + public IActionResult UpdateAttachmentWithTag(string org, string app, [FromQuery] string currentPage, [FromQuery] string layoutSetId, [FromQuery] string dataTypeId) + { + return Ok(); + } + + private static string GetSelectedLayoutSetInEditorFromRefererHeader(string refererHeader) + { + Uri refererUri = new(refererHeader); + string layoutSetName = HttpUtility.ParseQueryString(refererUri.Query)["selectedLayoutSet"]; + + return string.IsNullOrEmpty(layoutSetName) ? null : layoutSetName; + } + } +} diff --git a/backend/src/Designer/Controllers/PreviewController.cs b/backend/src/Designer/Controllers/PreviewController.cs index d91cd2076be..5a3c74245d3 100644 --- a/backend/src/Designer/Controllers/PreviewController.cs +++ b/backend/src/Designer/Controllers/PreviewController.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using System.Web; -using Altinn.App.Core.Internal.Process.Elements; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Enums; using Altinn.Platform.Register.Models; @@ -30,50 +29,40 @@ namespace Altinn.Studio.Designer.Controllers /// /// Controller containing all actions related to preview - still under development /// + /// + /// Initializes a new instance of the class. + /// + /// + /// IAltinnGitRepositoryFactory + /// Schema Model Service + /// Preview Service + /// Texts Service + /// App Development Service + /// Factory class that knows how to create types of [Authorize] [AutoValidateAntiforgeryToken] // Uses regex to not match on designer since the call from frontend to get the iframe for app-frontend, // `designer/html/preview.html`, will match on Image-endpoint which is a fetch-all route [Route("{org:regex(^(?!designer))}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}")] - public class PreviewController : Controller + public class PreviewController(IHttpContextAccessor httpContextAccessor, + IAltinnGitRepositoryFactory altinnGitRepositoryFactory, + ISchemaModelService schemaModelService, + IPreviewService previewService, + ITextsService textsService, + IAppDevelopmentService appDevelopmentService) : Controller { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory; - private readonly ISchemaModelService _schemaModelService; - private readonly IPreviewService _previewService; - private readonly ITextsService _textsService; - private readonly IAppDevelopmentService _appDevelopmentService; + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory = altinnGitRepositoryFactory; + private readonly ISchemaModelService _schemaModelService = schemaModelService; + private readonly IPreviewService _previewService = previewService; + private readonly ITextsService _textsService = textsService; + private readonly IAppDevelopmentService _appDevelopmentService = appDevelopmentService; // This value will be overridden to act as the task number for apps that use layout sets private const int PartyId = 51001; private const string MINIMUM_NUGET_VERSION = "8.0.0.0"; private const int MINIMUM_PREVIEW_NUGET_VERSION = 15; - /// - /// Initializes a new instance of the class. - /// - /// - /// IAltinnGitRepositoryFactory - /// Schema Model Service - /// Preview Service - /// Texts Service - /// App Development Service - /// Factory class that knows how to create types of - public PreviewController(IHttpContextAccessor httpContextAccessor, - IAltinnGitRepositoryFactory altinnGitRepositoryFactory, - ISchemaModelService schemaModelService, - IPreviewService previewService, - ITextsService textsService, - IAppDevelopmentService appDevelopmentService) - { - _httpContextAccessor = httpContextAccessor; - _altinnGitRepositoryFactory = altinnGitRepositoryFactory; - _schemaModelService = schemaModelService; - _previewService = previewService; - _textsService = textsService; - _appDevelopmentService = appDevelopmentService; - } - /// /// Default action for the preview. /// @@ -265,30 +254,30 @@ public async Task> LayoutSettingsForV4Apps(string org, stri } /// - /// Action for getting a response from v1/data/anonymous + /// Action for responding to keepAlive /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation. - /// Empty object + /// 200 Ok [HttpGet] - [Route("api/v1/data/anonymous")] - public IActionResult Anonymous(string org, string app) + [Route("api/authentication/keepAlive")] + public IActionResult KeepAlive(string org, string app) { - string user = "{}"; - return Content(user); + return Ok(); } /// - /// Action for responding to keepAlive + /// Action for getting a response from v1/data/anonymous /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation. - /// 200 Ok + /// Empty object [HttpGet] - [Route("api/authentication/keepAlive")] - public IActionResult KeepAlive(string org, string app) + [Route("api/v1/data/anonymous")] + public IActionResult Anonymous(string org, string app) { - return Ok(); + string user = "{}"; + return Content(user); } /// @@ -398,25 +387,6 @@ public IActionResult ValidateInstantiation() return Ok(textResource); } - /// - /// Action for creating the mocked instance object - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// - /// A that observes if operation is cancelled. - /// The mocked instance object - [HttpPost] - [Route("instances")] - public async Task> Instances(string org, string app, [FromQuery] int? instanceOwnerPartyId, CancellationToken cancellationToken) - { - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, instanceOwnerPartyId, layoutSetName, cancellationToken); - return Ok(mockInstance); - } - /// /// Action for getting the mocked instance id /// @@ -435,198 +405,6 @@ public async Task> GetInstanceId(string org, string app, Ca return Ok(mockInstance.Id); } - /// - /// Action for getting the json schema for the datamodel for the default data task test-datatask-id - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// party id - /// instance - /// A that observes if operation is cancelled. - /// Json schema for datamodel for the current task - [HttpGet] - [Route("instances/{partyId}/{instanceGuid}/data/" + PreviewService.MockDataTaskId)] - public async Task GetFormData(string org, string app, [FromRoute] int partyId, [FromRoute] string instanceGuid, CancellationToken cancellationToken) - { - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - DataType dataType = await _previewService.GetDataTypeForLayoutSetName(org, app, developer, layoutSetName, cancellationToken); - // For apps that does not have a datamodel - if (dataType == null) - { - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); - return Ok(mockInstance.Id); - } - string modelPath = $"/App/models/{dataType.Id}.schema.json"; - string decodedPath = Uri.UnescapeDataString(modelPath); - string formData = await _schemaModelService.GetSchema(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), decodedPath, cancellationToken); - return Ok(formData); - } - - /// - /// Action for updating the json schema for the datamodel for the current data task in the process - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// - /// - /// A that observes if operation is cancelled. - /// Json schema for datamodel for the current data task in the process - [HttpPut] - [Route("instances/{partyId}/{instanceGuid}/data/" + PreviewService.MockDataTaskId)] - public async Task UpdateFormData(string org, string app, [FromRoute] int partyId, [FromRoute] string instanceGuid, CancellationToken cancellationToken) - { - return await GetFormData(org, app, partyId, instanceGuid, cancellationToken); - } - - /// - /// Action for mocking upload of an attachment to an attachment component - /// - /// Id of the attachment component in application metadata - /// A 201 Created response with a mocked data element - [HttpPost] - [Route("instances/{partyId}/{instanceGuid}/data")] - public ActionResult PostAttachment([FromQuery] string dataType) - { - // This guid will be the unique id of the uploaded attachment - Guid guid = Guid.NewGuid(); - DataElement dataElement = new() { Id = guid.ToString() }; - return Created("link-to-app-placeholder", dataElement); - } - - /// - /// Action for mocking deleting an uploaded attachment to an attachment component - /// - /// Id of the attachment in application metadata - /// Ok - [HttpDelete] - [Route("instances/{partyId}/{instanceGuid}/data/{dataTypeId}")] - public ActionResult DeleteAttachment([FromRoute] string dataTypeId) - { - return Ok(); - } - - /// - /// Action for mocking updating tags for an attachment component in the datamodel - /// - /// The specific tag from the code list chosen for the attachment - /// Ok - [HttpPost] - [Route("instances/{partyId}/{instanceGuid}/data/{dataTypeId}/tags")] - public ActionResult UpdateTagsForAttachment([FromBody] string tag) - { - return Created("link-to-app-placeholder", tag); - } - - /// - /// Action for getting a mocked response for the current task connected to the instance - /// - /// The processState - [HttpGet] - [Route("instances/{partyId}/{instanceGuId}/process")] - public async Task> Process(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) - { - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); - List tasks = await _previewService.GetTasksForAllLayoutSets(org, app, developer, cancellationToken); - AppProcessState processState = new AppProcessState(mockInstance.Process) - { - ProcessTasks = tasks != null - ? new List(tasks?.ConvertAll(task => new AppProcessTaskTypeInfo { ElementId = task, AltinnTaskType = "data" })) - : null - }; - - return Ok(processState); - } - - /// - /// Endpoint to get instance for next process step - /// - /// A mocked instance object - [HttpGet] - [Route("instances/{partyId}/{instanceGuId}")] - public async Task> InstanceForNextTask(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) - { - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); - return Ok(mockInstance); - } - - /// - /// Endpoint to get active instances for apps with state/layout sets/multiple processes - /// - /// A list of a single mocked instance - [HttpGet] - [Route("instances/{partyId}/active")] - public ActionResult> ActiveInstancesForAppsWithLayoutSets(string org, string app, [FromRoute] int partyId) - { - // Simulate never having any active instances - List activeInstances = new(); - return Ok(activeInstances); - } - - /// - /// Endpoint to validate an instance - /// - /// Ok - [HttpGet] - [Route("instances/{partyId}/{instanceGuId}/validate")] - public ActionResult ValidateInstance() - { - return Ok(); - } - - /// - /// Endpoint to validate a data task for an instance - /// - /// Ok - [HttpGet] - [Route("instances/{partyId}/{instanceGuId}/data/" + PreviewService.MockDataTaskId + "/validate")] - public ActionResult ValidateInstanceForDataTask() - { - return Ok(new List()); - } - - /// - /// Action for getting a mocked response for the next task connected to the instance - /// - /// The processState object on the global mockInstance object - [HttpGet] - [Route("instances/{partyId}/{instanceGuId}/process/next")] - public async Task ProcessNext(string org, string app, [FromRoute] int partyId, CancellationToken cancellationToken) - { - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); - return Ok(mockInstance.Process); - } - - /// - /// Action for mocking an end to the process in order to get receipt after "send inn" is pressed - /// - /// Process object where ended is set - [HttpPut] - [Route("instances/{partyId}/{instanceGuId}/process/next")] - public async Task UpdateProcessNext(string org, string app, [FromRoute] int partyId, [FromQuery] string lang, CancellationToken cancellationToken) - { - string refererHeader = Request.Headers["Referer"]; - string layoutSetName = GetSelectedLayoutSetInEditorFromRefererHeader(refererHeader); - if (string.IsNullOrEmpty(layoutSetName)) - { - string endProcess = """{"ended": "ended"}"""; - return Ok(endProcess); - } - string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext); - Instance mockInstance = await _previewService.GetMockInstance(org, app, developer, partyId, layoutSetName, cancellationToken); - return Ok(mockInstance.Process); - } - /// /// Action for mocking a response to getting all text resources /// @@ -866,70 +644,6 @@ public async Task> GetOptions(string org, string app, strin } } - /// - /// Action for getting options list for a given options list id for a given instance - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The id of the options list - /// The language for the options list - /// The source of the options list - /// A that observes if operation is cancelled. - /// The options list if it exists, otherwise nothing - [HttpGet] - [Route("instances/{partyId}/{instanceGuid}/options/{optionListId}")] - public async Task> GetOptionsForInstance(string org, string app, string optionListId, [FromQuery] string language, [FromQuery] string source, CancellationToken cancellationToken) - { - try - { - // TODO: Need code to get dynamic options list based on language and source? - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer); - string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken); - return Ok(options); - } - catch (NotFoundException) - { - // Return empty list since app-frontend don't handle a null result - return Ok(new List()); - } - } - - /// - /// Action for getting data list for a given data list id for a given instance - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The id of the data list - /// The language for the data list - /// The number of items to return - /// A that observes if operation is cancelled. - /// The options list if it exists, otherwise nothing - [HttpGet] - [Route("instances/{partyId}/{instanceGuid}/datalists/{dataListId}")] - public ActionResult> GetDataListsForInstance(string org, string app, string dataListId, [FromQuery] string language, [FromQuery] string size, CancellationToken cancellationToken) - { - // TODO: Should look into whether we can get some actual data here, or if we can make an "informed" mock based on the setup. - // For now, we just return an empty list. - return Ok(new List()); - } - - /// - /// Action for updating data model with tag for attachment component // TODO: Figure out what actually happens here - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// Current page in running app - /// Current layout set in running app - /// Connected datatype for that process task - /// The options list if it exists, otherwise nothing - [HttpPost] - [Route("instances/{partyId}/{instanceGuid}/pages/order")] - public IActionResult UpdateAttachmentWithTag(string org, string app, [FromQuery] string currentPage, [FromQuery] string layoutSetId, [FromQuery] string dataTypeId) - { - return Ok(); - } - /// /// Action for mocking the GET method for app footer /// diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs index e85ecd0f2bf..765c1a26b2a 100644 --- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs +++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs @@ -12,8 +12,10 @@ using Altinn.Studio.Designer.Repository.ORMImplementation; using Altinn.Studio.Designer.Repository.ORMImplementation.Data; using Altinn.Studio.Designer.Services.Implementation; +using Altinn.Studio.Designer.Services.Implementation.Preview; using Altinn.Studio.Designer.Services.Implementation.ProcessModeling; using Altinn.Studio.Designer.Services.Interfaces; +using Altinn.Studio.Designer.Services.Interfaces.Preview; using Altinn.Studio.Designer.TypedHttpClients.ImageClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -75,6 +77,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol services.AddHttpClient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/backend/src/Designer/Models/Preview/DataPatchRequest.cs b/backend/src/Designer/Models/Preview/DataPatchRequest.cs new file mode 100644 index 00000000000..033279247b1 --- /dev/null +++ b/backend/src/Designer/Models/Preview/DataPatchRequest.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Json.Patch; + +namespace Altinn.Studio.Designer.Models.Preview; + +/// +/// Represents the request to patch data on the . +/// +public class DataPatchRequest +{ + /// + /// The Patch operation to perform. + /// + [JsonPropertyName("patch")] + public required JsonPatch Patch { get; init; } + + /// + /// List of validators to ignore during the patch operation. + /// Issues from these validators will not be run during the save operation, but the validator will run on process/next + /// + [JsonPropertyName("ignoredValidators")] + public required List? IgnoredValidators { get; init; } +} diff --git a/backend/src/Designer/Models/Preview/DataPatchResponse.cs b/backend/src/Designer/Models/Preview/DataPatchResponse.cs new file mode 100644 index 00000000000..9e794866fd3 --- /dev/null +++ b/backend/src/Designer/Models/Preview/DataPatchResponse.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Altinn.App.Core.Models.Validation; + +namespace Altinn.Studio.Designer.Models.Preview; + +/// +/// Represents the response from a data patch operation on the . +/// +public class DataPatchResponse +{ + /// + /// The validation issues that were found during the patch operation. + /// + [JsonPropertyName("validationIssues")] + public required Dictionary> ValidationIssues { get; init; } + + /// + /// The current data model after the patch operation. + /// + [JsonPropertyName("newDataModel")] + public required object NewDataModel { get; init; } +} diff --git a/backend/src/Designer/Services/Implementation/Preview/DataService.cs b/backend/src/Designer/Services/Implementation/Preview/DataService.cs new file mode 100644 index 00000000000..75a00b64155 --- /dev/null +++ b/backend/src/Designer/Services/Implementation/Preview/DataService.cs @@ -0,0 +1,57 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using Altinn.Platform.Storage.Interface.Models; +using Altinn.Studio.Designer.Services.Interfaces.Preview; +using Json.Patch; +using Microsoft.Extensions.Caching.Distributed; + +namespace Altinn.Studio.Designer.Services.Implementation.Preview; + +public class DataService( + IDistributedCache distributedCache + ) : IDataService +{ + readonly DistributedCacheEntryOptions _cacheOptions = new() + { + SlidingExpiration = TimeSpan.FromMinutes(30), + }; + + public DataElement CreateDataElement(int partyId, Guid instanceGuid, string dataTypeId) + { + Guid dataElementGuid = Guid.NewGuid(); + DataElement dataElement = new() + { + Id = dataElementGuid.ToString(), + DataType = dataTypeId, + InstanceGuid = instanceGuid.ToString(), + Created = DateTime.Now, + CreatedBy = partyId.ToString(), + }; + + distributedCache.SetString(dataElementGuid.ToString(), "{}", _cacheOptions); + return dataElement; + } + + public JsonNode GetDataElement(Guid dataGuid) + { + string dataElementJson = distributedCache.GetString(dataGuid.ToString()); + JsonNode dataElement = JsonSerializer.Deserialize(dataElementJson); + return dataElement; + } + + public JsonNode PatchDataElement(Guid dataGuid, JsonPatch patch) + { + string dataJson = distributedCache.GetString(dataGuid.ToString()); + JsonNode dataNode = JsonSerializer.Deserialize(dataJson); + PatchResult patchResult = patch.Apply(dataNode); + if (!patchResult.IsSuccess) + { + throw new InvalidOperationException("Patch operation failed." + patchResult.Error); + } + dataNode = patchResult.Result; + distributedCache.SetString(dataGuid.ToString(), JsonSerializer.Serialize(dataNode), _cacheOptions); + return dataNode; + } + +} diff --git a/backend/src/Designer/Services/Interfaces/Preview/IDataService.cs b/backend/src/Designer/Services/Interfaces/Preview/IDataService.cs new file mode 100644 index 00000000000..e12c80a05bf --- /dev/null +++ b/backend/src/Designer/Services/Interfaces/Preview/IDataService.cs @@ -0,0 +1,16 @@ +using System; +using System.Text.Json.Nodes; +using Altinn.Platform.Storage.Interface.Models; +using Json.Patch; + +namespace Altinn.Studio.Designer.Services.Interfaces.Preview; + +/// +/// Interface for handling a mocked datatype object for preview mode +/// +public interface IDataService +{ + public DataElement CreateDataElement(int partyId, Guid instanceGuid, string dataTypeId); + public JsonNode GetDataElement(Guid dataGuid); + public JsonNode PatchDataElement(Guid dataGuid, JsonPatch patch); +} diff --git a/backend/tests/Designer.Tests/Services/Preview/DataServiceTest.cs b/backend/tests/Designer.Tests/Services/Preview/DataServiceTest.cs new file mode 100644 index 00000000000..ee8c5e6d450 --- /dev/null +++ b/backend/tests/Designer.Tests/Services/Preview/DataServiceTest.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.Json.Nodes; +using Altinn.Platform.Storage.Interface.Models; +using Altinn.Studio.Designer.Services.Implementation.Preview; +using Altinn.Studio.Designer.Services.Interfaces.Preview; +using Json.Patch; +using Json.Pointer; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Designer.Tests.Services.Preview +{ + public class DataServiceTest + { + private readonly IDataService _dataService; + + public DataServiceTest() + { + ServiceCollection serviceCollection = new(); + serviceCollection.AddDistributedMemoryCache(); + ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + IDistributedCache distributedCache = serviceProvider.GetRequiredService(); + _dataService = new DataService(distributedCache); + } + + [Theory] + [InlineData(1234, "f1e23d45-6789-1bcd-8c34-56789abcdef0", "dataType1423")] + public void CreateDataElement_ReturnsCorrectDataElement(int partyId, Guid instanceGuid, string dataType) + { + DataElement dataElement = _dataService.CreateDataElement(partyId, instanceGuid, dataType); + Assert.Equal(dataElement.CreatedBy, partyId.ToString()); + Assert.Equal(dataElement.DataType, dataType); + Assert.Equal(dataElement.InstanceGuid, instanceGuid.ToString()); + Assert.NotNull(dataElement.Id); + } + + [Theory] + [InlineData(1234, "f1e23d45-6789-1bcd-8c34-56789abcdef0", "dataType1423")] + public void GetDataElement_ReturnsCorrectDataObject(int partyId, Guid instanceGuid, string dataType) + { + DataElement dataElement = _dataService.CreateDataElement(partyId, instanceGuid, dataType); + JsonNode dataObject = _dataService.GetDataElement(new Guid(dataElement.Id)); + Console.WriteLine(dataObject); + Assert.NotNull(dataObject); + } + + [Theory] + [InlineData(1234, "f1e23d45-6789-1bcd-8c34-56789abcdef0", "dataType1423", "testProperty", "testValue")] + public void PatchDataElement_UpdatesObject(int partyId, Guid instanceGuid, string dataType, string testProperty, string testPropertyValue) + { + DataElement dataElement = _dataService.CreateDataElement(partyId, instanceGuid, dataType); + JsonNode dataObject = _dataService.GetDataElement(new Guid(dataElement.Id)); + Assert.NotNull(dataObject); + JsonNode patchedObject = _dataService.PatchDataElement(new Guid(dataElement.Id), new JsonPatch(PatchOperation.Add(JsonPointer.Parse($"/{testProperty}"), testPropertyValue))); + Assert.NotNull(patchedObject); + Assert.Equal(testPropertyValue, patchedObject[testProperty].ToString()); + } + } +}