Skip to content

Commit

Permalink
Implement IInstanceDataMutator.AbandonAllChanges (#870)
Browse files Browse the repository at this point in the history
* Modify Delete data element to return patch result and validations

* Implement IInstanceDataMutator.AbandonAllChanges

This returns validation issues in a ProblemDetails object with status 400 BadRequest status
  • Loading branch information
ivarne authored Oct 31, 2024
1 parent 6bac8a1 commit caf1866
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 115 deletions.
7 changes: 7 additions & 0 deletions src/Altinn.App.Api/Controllers/ActionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ await _appMetadata.GetApplicationMetadata(),
);
}

if (dataMutator.AbandonIssues.Count > 0)
{
throw new NotImplementedException(
"return an error response from UserActions instead of abandoning the dataMutator"
);
}

var changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);

await dataMutator.UpdateInstanceData(changes);
Expand Down
212 changes: 129 additions & 83 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,60 @@ await _prefillService.PrefillDataModel(
}
else
{
var createBinaryResult = await CreateBinaryData(dataMutator, dataType);
if (!createBinaryResult.Success)
(bool validationRestrictionSuccess, List<ValidationIssue> errors) =
DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType);

if (!validationRestrictionSuccess)
{
var issuesWithSource = errors
.Select(e => ValidationIssueWithSource.FromIssue(e, "DataRestrictionValidation", false))
.ToList();
return new DataPostErrorResponse("Common checks failed", issuesWithSource);
}

if (Request.ContentType is null)
{
return new ProblemDetails()
{
Title = "Missing content type",
Detail = "The request is missing a content type header.",
Status = (int)HttpStatusCode.BadRequest,
};
}

var (bytes, actualLength) = await Request.ReadBodyAsByteArrayAsync(dataType.MaxSize * 1024 * 1024);
if (bytes is null)
{
return new ProblemDetails()
{
Title = "Request too large",
Detail =
$"The request body is too large. The content length is {actualLength} bytes, which exceeds the limit of {dataType.MaxSize} MB",
Status = (int)HttpStatusCode.RequestEntityTooLarge,
};
}

if (bytes.Length is 0)
{
return new ProblemDetails()
{
Title = "Invalid data",
Detail = "Invalid data provided. Error: The file is zero bytes.",
Status = (int)HttpStatusCode.BadRequest,
};
}

bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues);
string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null;

var analysisAndValidationProblem = await RunFileAnalysisAndValidation(dataType, bytes, filename);
if (analysisAndValidationProblem != null)
{
return createBinaryResult.Error;
return analysisAndValidationProblem;
}

//schedule the binary data element to be created
dataMutator.AddBinaryDataElement(dataType.Id, Request.ContentType, filename, bytes);
}

var initialChanges = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
Expand All @@ -310,20 +359,23 @@ await _prefillService.PrefillDataModel(
throw new InvalidOperationException("Expected exactly one change in initialChanges");
}

// Mutates initialChanges and to add DataElement Type = Created
await dataMutator.UpdateInstanceData(initialChanges);

var newDataElement =
change.DataElement
?? throw new InvalidOperationException("DataElement not set in dataMutator.UpdateInstanceData");

await _patchService.RunDataProcessors(
dataMutator,
initialChanges,
instance.Process.CurrentTask.ElementId,
language
);

if (dataMutator.AbandonIssues.Count > 1)
{
return new DataPostErrorResponse(
"Data Processing abandoned",
dataMutator
.AbandonIssues.Select(i => ValidationIssueWithSource.FromIssue(i, "dataProcessing", true))
.ToList()
);
}

var finalChanges = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
await dataMutator.UpdateInstanceData(finalChanges);
var saveTask = dataMutator.SaveChanges(finalChanges);
Expand All @@ -346,16 +398,13 @@ await _patchService.RunDataProcessors(
await saveTask;
SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request);

var newDataElement =
change.DataElement ?? throw new InvalidOperationException("The change was not updated while saving");
return new DataPostResponse
{
Instance = instance,
NewDataElementId = Guid.Parse(newDataElement.Id),
NewDataModels = finalChanges
.FormDataChanges.Select(formDataChange => new DataModelPairResponse(
formDataChange.DataElementIdentifier.Guid,
formDataChange.CurrentFormData
))
.ToList(),
NewDataModels = GetNewDataModels(finalChanges),
ValidationIssues = validationIssues,
};
}
Expand All @@ -370,63 +419,16 @@ await _patchService.RunDataProcessors(
}
}

private async Task<ServiceResult<BinaryDataChange, ProblemDetails>> CreateBinaryData(
InstanceDataUnitOfWork dataMutator,
DataType dataTypeFromMetadata
)
private static List<DataModelPairResponse> GetNewDataModels(DataElementChanges finalChanges)
{
(bool validationRestrictionSuccess, List<ValidationIssue> errors) =
DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata);

if (!validationRestrictionSuccess)
{
var issuesWithSource = errors
.Select(e => ValidationIssueWithSource.FromIssue(e, "DataRestrictionValidation", false))
.ToList();
return new DataPostErrorResponse("Common checks failed", issuesWithSource);
}

if (Request.ContentType is null)
{
return new ProblemDetails()
{
Title = "Missing content type",
Detail = "The request is missing a content type header.",
Status = (int)HttpStatusCode.BadRequest,
};
}

var (bytes, actualLength) = await Request.ReadBodyAsByteArrayAsync(dataTypeFromMetadata.MaxSize * 1024 * 1024);
if (bytes is null)
{
return new ProblemDetails()
{
Title = "Request too large",
Detail =
$"The request body is too large. The content length is {actualLength} bytes, which exceeds the limit of {dataTypeFromMetadata.MaxSize} MB",
Status = (int)HttpStatusCode.RequestEntityTooLarge,
};
}
if (bytes.Length is 0)
{
return new ProblemDetails()
{
Title = "Invalid data",
Detail = "Invalid data provided. Error: The file is zero bytes.",
Status = (int)HttpStatusCode.BadRequest,
};
}

bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues);
string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null;

var analysisAndValidationProblem = await RunFileAnalysisAndValidation(dataTypeFromMetadata, bytes, filename);
if (analysisAndValidationProblem != null)
{
return analysisAndValidationProblem;
}

return dataMutator.AddBinaryDataElement(dataTypeFromMetadata.Id, Request.ContentType, filename, bytes);
// Return currentFormData for form data changes that are created or updated
return finalChanges
.FormDataChanges.Where(c => c.Type != ChangeType.Deleted)
.Select(formDataChange => new DataModelPairResponse(
formDataChange.DataElementIdentifier.Guid,
formDataChange.CurrentFormData
))
.ToList();
}

private async Task<ProblemDetails?> RunFileAnalysisAndValidation(
Expand Down Expand Up @@ -726,9 +728,7 @@ public async Task<ActionResult<DataPatchResponseMultiple>> PatchFormDataMultiple
new DataPatchResponseMultiple()
{
Instance = res.Ok.Instance,
NewDataModels = res
.Ok.UpdatedData.Select(d => new DataModelPairResponse(d.Identifier.Guid, d.Data))
.ToList(),
NewDataModels = GetNewDataModels(res.Ok.FormDataChanges),
ValidationIssues = res.Ok.ValidationIssues
}
);
Expand All @@ -753,16 +753,18 @@ public async Task<ActionResult<DataPatchResponseMultiple>> PatchFormDataMultiple
/// <param name="instanceOwnerPartyId">unique id of the party that is the owner of the instance</param>
/// <param name="instanceGuid">unique id to identify the instance</param>
/// <param name="dataGuid">unique id to identify the data element to update</param>
/// <param name="ignoredValidators">comma separated string of validators to ignore</param>
/// <param name="language">The currently active language</param>
/// <returns>The updated data element.</returns>
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)]
[HttpDelete("{dataGuid:guid}")]
public async Task<ActionResult> Delete(
public async Task<ActionResult<DataPostResponse>> Delete(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] int instanceOwnerPartyId,
[FromRoute] Guid instanceGuid,
[FromRoute] Guid dataGuid,
[FromQuery] string? ignoredValidators = null,
[FromQuery] string? language = null
)
{
Expand Down Expand Up @@ -798,12 +800,43 @@ await _appMetadata.GetApplicationMetadata(),
var changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: false);
await _patchService.RunDataProcessors(dataMutator, changes, taskId, language);

if (dataMutator.AbandonIssues.Count > 1)
{
return BadRequest(
new DataPostErrorResponse(
"DataProcessing abandoned",
dataMutator
.AbandonIssues.Select(i => ValidationIssueWithSource.FromIssue(i, "dataProcessing", true))
.ToList()
)
);
}
// Get the updated changes for saving
changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: false);
await dataMutator.UpdateInstanceData(changes);
await dataMutator.SaveChanges(changes);

return Ok();
List<ValidationSourcePair> validationIssues = [];
if (ignoredValidators is not null)
{
var ignoredValidatorsList = ignoredValidators.Split(',').Where(v => !string.IsNullOrEmpty(v)).ToList();
validationIssues = await _patchService.RunIncrementalValidation(
dataMutator,
instance.Process.CurrentTask.ElementId,
changes,
ignoredValidatorsList,
language
);
}

return Ok(
new DataDeleteResponse()
{
Instance = instance,
ValidationIssues = validationIssues,
NewDataModels = GetNewDataModels(changes),
}
);
}
catch (PlatformHttpException e)
{
Expand Down Expand Up @@ -1072,6 +1105,18 @@ await _patchService.RunDataProcessors(
);
var jsonAfterDataProcessors = JsonSerializer.Serialize(serviceModel);

if (dataMutator.AbandonIssues.Count > 1)
{
return BadRequest(
new DataPostErrorResponse(
"DataProcessing abandoned",
dataMutator
.AbandonIssues.Select(i => ValidationIssueWithSource.FromIssue(i, "dataProcessing", true))
.ToList()
)
);
}

// Save changes
var changesAfterDataProcessors = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
await dataMutator.UpdateInstanceData(changesAfterDataProcessors);
Expand Down Expand Up @@ -1110,21 +1155,22 @@ private ActionResult HandlePlatformHttpException(PlatformHttpException e, string

private ObjectResult Problem(DataPatchError error)
{
int code = error.ErrorType switch
var code = error.ErrorType switch
{
DataPatchErrorType.PatchTestFailed => (int)HttpStatusCode.Conflict,
DataPatchErrorType.DeserializationFailed => (int)HttpStatusCode.UnprocessableContent,
_ => (int)HttpStatusCode.InternalServerError
DataPatchErrorType.PatchTestFailed => HttpStatusCode.Conflict,
DataPatchErrorType.DeserializationFailed => HttpStatusCode.UnprocessableContent,
DataPatchErrorType.AbandonedRequest => HttpStatusCode.BadRequest,
_ => HttpStatusCode.InternalServerError
};

return StatusCode(
code,
(int)code,
new ProblemDetails()
{
Title = error.Title,
Detail = error.Detail,
Type = "https://datatracker.ietf.org/doc/html/rfc6902/",
Status = code,
Status = (int)code,
Extensions = error.Extensions ?? new Dictionary<string, object?>(StringComparer.Ordinal)
}
);
Expand Down
19 changes: 19 additions & 0 deletions src/Altinn.App.Api/Controllers/InstancesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,25 @@ string action
var changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
await _patchService.RunDataProcessors(dataMutator, changes, taskId, language);

if (dataMutator.AbandonIssues.Count > 0)
{
_logger.LogWarning(
"Data processing failed for one or more data elements, the instance was created, but we try to delete the instance"
);
await _instanceClient.DeleteInstance(
int.Parse(instance.Id.Split("/")[0], CultureInfo.InvariantCulture),
Guid.Parse(instance.Id.Split("/")[1]),
hard: true
);
return new ProblemDetails
{
Title = "Data processing failed",
Detail =
"Data processing failed for one or more data elements, the instance was created, but the data was ignored",
Extensions = { { "issues", dataMutator.AbandonIssues }, { "instance", instance } }
};
}

// Update the changes list if it changed in data processors
changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
await dataMutator.UpdateInstanceData(changes);
Expand Down
29 changes: 29 additions & 0 deletions src/Altinn.App.Api/Models/DataDeleteResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
using Altinn.App.Core.Models.Validation;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Api.Models;

/// <summary>
/// Response object for POST to /org/app/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataType}
/// </summary>
public class DataDeleteResponse
{
/// <summary>
/// The instance with updated data
/// </summary>
[JsonPropertyName("instance")]
public required Instance Instance { get; init; }

/// <summary>
/// List of validation issues that reported to have relevant changes after a new data element was added
/// </summary>
[JsonPropertyName("validationIssues")]
public required List<ValidationSourcePair> ValidationIssues { get; init; }

/// <summary>
/// List of updated DataModels caused by dataProcessing
/// </summary>
[JsonPropertyName("newDataModels")]
public required List<DataModelPairResponse> NewDataModels { get; init; }
}
Loading

0 comments on commit caf1866

Please sign in to comment.