Skip to content

Commit

Permalink
Use IDataWriteProcessor in all apis to mutate data elements.
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarne committed Oct 25, 2024
1 parent 0303701 commit fd6ed52
Show file tree
Hide file tree
Showing 17 changed files with 836 additions and 456 deletions.
276 changes: 128 additions & 148 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,17 +298,25 @@ await _prefillService.PrefillDataModel(
}
else
{
var problem = await CreateBinaryData(dataMutator, dataType);
if (problem is not null)
var createBinaryResult = await CreateBinaryData(dataMutator, dataType);
if (!createBinaryResult.Success)
{
return problem;
return createBinaryResult.Error;
}
}

// Save the posted data and get data element;
var initialChanges = dataMutator.GetDataElementChanges(initializeAltinnRowId: true);
var newDataElements = await dataMutator.UpdateInstanceData(initialChanges);
var newDataElement = newDataElements.Single(); // Get the single created element
if (initialChanges is not { AllChanges: [var change] })
{
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,
Expand Down Expand Up @@ -344,9 +352,9 @@ await _patchService.RunDataProcessors(
Instance = instance,
NewDataElementId = Guid.Parse(newDataElement.Id),
NewDataModels = finalChanges
.FormDataChanges.Select(change => new DataModelPairResponse(
change.DataElementIdentifier.Guid,
change.CurrentFormData
.FormDataChanges.Select(formDataChange => new DataModelPairResponse(
formDataChange.DataElementIdentifier.Guid,
formDataChange.CurrentFormData
))
.ToList(),
ValidationIssues = validationIssues,
Expand All @@ -363,7 +371,7 @@ await _patchService.RunDataProcessors(
}
}

private async Task<ProblemDetails?> CreateBinaryData(
private async Task<ServiceResult<BinaryChange, ProblemDetails>> CreateBinaryData(
InstanceDataUnitOfWork dataMutator,
DataType dataTypeFromMetadata
)
Expand All @@ -379,6 +387,16 @@ DataType dataTypeFromMetadata
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)
{
Expand All @@ -403,6 +421,21 @@ DataType dataTypeFromMetadata
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);
}

private async Task<ProblemDetails?> RunFileAnalysisAndValidation(
DataType dataTypeFromMetadata,
byte[] bytes,
string? filename
)
{
List<FileAnalysisResult> fileAnalysisResults = new List<FileAnalysisResult>();
if (FileAnalysisEnabledForDataType(dataTypeFromMetadata))
{
Expand All @@ -425,17 +458,7 @@ DataType dataTypeFromMetadata
{
return new DataPostErrorResponse("File validation failed", validationIssues);
}
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,
};
}

dataMutator.AddAttachmentDataElement(dataTypeFromMetadata.Id, Request.ContentType, filename, bytes);
return null;
}

Expand Down Expand Up @@ -578,21 +601,7 @@ public async Task<ActionResult> Put(
return await PutFormData(instance, dataElement, dataType, language);
}

(bool validationRestrictionSuccess, List<ValidationIssue> errors) =
DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType);

if (!validationRestrictionSuccess)
{
return BadRequest(
await GetErrorDetails(
errors
.Select(e => ValidationIssueWithSource.FromIssue(e, "DataRestrictionValidation", false))
.ToList()
)
);
}

return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid);
return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid, dataType);
}
catch (PlatformHttpException e)
{
Expand Down Expand Up @@ -745,6 +754,7 @@ 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="language">The currently active language</param>
/// <returns>The updated data element.</returns>
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)]
[HttpDelete("{dataGuid:guid}")]
Expand All @@ -753,7 +763,8 @@ public async Task<ActionResult> Delete(
[FromRoute] string app,
[FromRoute] int instanceOwnerPartyId,
[FromRoute] Guid instanceGuid,
[FromRoute] Guid dataGuid
[FromRoute] Guid dataGuid,
[FromQuery] string? language = null
)
{
try
Expand All @@ -763,14 +774,37 @@ [FromRoute] Guid dataGuid
{
return Problem(instanceResult.Error);
}
var (instance, dataType, _) = instanceResult.Ok;
var (instance, dataType, dataElement) = instanceResult.Ok;

if (DataElementAccessChecker.GetDeleteProblem(instance, dataType, dataGuid, User) is { } accessProblem)
{
return Problem(accessProblem);
}

return await DeleteBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid);
var taskId =
instance.Process?.CurrentTask?.ElementId
?? throw new InvalidOperationException("Instance have no process");

var dataMutator = new InstanceDataUnitOfWork(
instance,
_dataClient,
_instanceClient,
await _appMetadata.GetApplicationMetadata(),
_modelDeserializer
);

dataMutator.RemoveDataElement(dataElement);

// Get the single change for running data processors
var changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: false);
await _patchService.RunDataProcessors(dataMutator, changes, taskId, language);

// Get the updated changes for saving
changes = dataMutator.GetDataElementChanges(initializeAltinnRowId: false);
await dataMutator.UpdateInstanceData(changes);
await dataMutator.SaveChanges(changes);

return Ok();
}
catch (PlatformHttpException e)
{
Expand Down Expand Up @@ -798,37 +832,6 @@ private ObjectResult ExceptionResponse(Exception exception, string message)
return StatusCode((int)HttpStatusCode.InternalServerError, $"{message}");
}

private async Task<ActionResult> CreateBinaryData(
Instance instanceBefore,
string dataType,
string contentType,
string? filename,
Stream fileStream
)
{
int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0], CultureInfo.InvariantCulture);
Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]);

DataElement dataElement = await _dataClient.InsertBinaryData(
instanceBefore.Id,
dataType,
contentType,
filename,
fileStream
);

if (Guid.Parse(dataElement.Id) == Guid.Empty)
{
return StatusCode(
(int)HttpStatusCode.InternalServerError,
$"Cannot store form attachment on instance {instanceOwnerPartyId}/{instanceGuid}"
);
}

SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request);
return Created(dataElement.SelfLinks.Apps, dataElement);
}

/// <summary>
/// Gets a data element from storage.
/// </summary>
Expand Down Expand Up @@ -858,34 +861,6 @@ DataElement dataElement
return NotFound();
}

private async Task<ActionResult> DeleteBinaryData(
string org,
string app,
int instanceOwnerId,
Guid instanceGuid,
Guid dataGuid
)
{
bool successfullyDeleted = await _dataClient.DeleteData(
org,
app,
instanceOwnerId,
instanceGuid,
dataGuid,
false
);

if (successfullyDeleted)
{
return Ok();
}

return StatusCode(
(int)HttpStatusCode.InternalServerError,
$"Something went wrong when deleting data element {dataGuid} for instance {instanceGuid}"
);
}

private async Task<DataType?> GetDataType(DataElement element)
{
Application application = await _appMetadata.GetApplicationMetadata();
Expand Down Expand Up @@ -981,25 +956,68 @@ await _dataClient.UpdateData(
return Ok(appModel);
}

private async Task<ActionResult> PutBinaryData(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid)
private async Task<ActionResult> PutBinaryData(
int instanceOwnerPartyId,
Guid instanceGuid,
Guid dataGuid,
DataType dataType
)
{
if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues))
//TODO: Consider having a rule that disables PUT for binary data elements.

(bool validationRestrictionSuccess, List<ValidationIssue> errors) =
DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType);

if (!validationRestrictionSuccess)
{
var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString());
_logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString());
DataElement dataElement = await _dataClient.UpdateBinaryData(
new InstanceIdentifier(instanceOwnerPartyId, instanceGuid),
Request.ContentType,
contentDispositionHeader.FileName.ToString(),
dataGuid,
Request.Body
return BadRequest(
await GetErrorDetails(
errors
.Select(e => ValidationIssueWithSource.FromIssue(e, "DataRestrictionValidation", false))
.ToList()
)
);
SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request);
}

return Created(dataElement.SelfLinks.Apps, dataElement);
if (!Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues))

Check warning

Code scanning / SonarCloud

Use model binding instead of reading raw request data Medium

Use model binding instead of accessing the raw request data See more on SonarCloud
{
return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header");
}

var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString());
_logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString());

var (bytes, actualLength) = await Request.ReadBodyAsByteArrayAsync(
dataType.MaxSize * 1024 * 1024 ?? REQUEST_SIZE_LIMIT
);
if (bytes is null)
{
return BadRequest(
$"The request body is too large. The content length is {actualLength} bytes, which exceeds the limit of {dataType.MaxSize} MB"
);
}

var analysisAndValidationProblem = await RunFileAnalysisAndValidation(
dataType,
bytes,
contentDispositionHeader.FileName.ToString()
);
if (analysisAndValidationProblem != null)
{
return Problem(analysisAndValidationProblem);
}

return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header");
DataElement dataElement = await _dataClient.UpdateBinaryData(
new InstanceIdentifier(instanceOwnerPartyId, instanceGuid),
Request.ContentType,
contentDispositionHeader.FileName.ToString(),
dataGuid,
new MemoryAsStream(bytes)
);

SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request);

return Created(dataElement.SelfLinks.Apps, dataElement);
}

private async Task<ActionResult> PutFormData(
Expand Down Expand Up @@ -1080,44 +1098,6 @@ await _patchService.RunDataProcessors(
return Created(dataUrl, dataElement);
}

private async Task UpdatePresentationTextsOnInstance(Instance instance, string dataType, object serviceModel)
{
var updatedValues = DataHelper.GetUpdatedDataValues(
(await _appMetadata.GetApplicationMetadata()).PresentationFields,
instance.PresentationTexts,
dataType,
serviceModel
);

if (updatedValues.Count > 0)
{
await _instanceClient.UpdatePresentationTexts(
int.Parse(instance.Id.Split("/")[0], CultureInfo.InvariantCulture),
Guid.Parse(instance.Id.Split("/")[1]),
new PresentationTexts { Texts = updatedValues }
);
}
}

private async Task UpdateDataValuesOnInstance(Instance instance, string dataType, object serviceModel)
{
var updatedValues = DataHelper.GetUpdatedDataValues(
(await _appMetadata.GetApplicationMetadata()).DataFields,
instance.DataValues,
dataType,
serviceModel
);

if (updatedValues.Count > 0)
{
await _instanceClient.UpdateDataValues(
int.Parse(instance.Id.Split("/")[0], CultureInfo.InvariantCulture),
Guid.Parse(instance.Id.Split("/")[1]),
new DataValues { Values = updatedValues }
);
}
}

private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage)
{
return e.Response.StatusCode switch
Expand Down
Loading

0 comments on commit fd6ed52

Please sign in to comment.