Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multi data model with validation #730

Open
wants to merge 69 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
cf1bbf5
Removes form-data delete check
danielskovli Jul 9, 2024
7798803
Implements `AllowUserCreate` and `AllowUserDelete` methodology. Fixes…
danielskovli Jul 16, 2024
ad56781
Temp project ref to Storage.Interface
danielskovli Jul 16, 2024
5484854
Access testing for DataController -> `Create` and `Delete`
danielskovli Jul 17, 2024
87cae85
Verifies MaxCount before creating a new data element
danielskovli Jul 24, 2024
ddc9199
Merge branch 'main' into feature/subform
danielskovli Jul 31, 2024
8d9d77d
Implement support for multiple data models in expressions
ivarne Aug 15, 2024
a688536
Restructure validation to support incremental validation for multiple…
ivarne Aug 15, 2024
f7b91e0
Fix tests and cleanup
ivarne Aug 19, 2024
e6cda48
Fix sonar cloud issues
ivarne Aug 19, 2024
3c4ac0c
Add tests for shadow fields and hidden component data removal in proc…
ivarne Aug 20, 2024
c27f627
chore: update to new storage nuget
HauklandJ Aug 21, 2024
79a918b
Improve testing for multiple patch endpoint
ivarne Aug 22, 2024
9007d42
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Aug 22, 2024
62356fb
Merge branch 'main' into feat/multi-data-model-with-validation
ivarne Aug 26, 2024
e23d1d2
update Storage.Interface package reference to 3.33.0
HauklandJ Aug 28, 2024
ac353e4
Merge remote-tracking branch 'origin/main' into feature/subform
danielskovli Aug 29, 2024
af1200b
Fixes some tests that were double-creating the `default` data element
danielskovli Aug 29, 2024
07c1af7
Reorders DataController->MaxCount logic, adds missing ClassRef check …
danielskovli Aug 29, 2024
300faac
More changes to make tests work after merge
ivarne Aug 30, 2024
4734382
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Aug 30, 2024
45d2108
Introduces `LayoutSet.Type` and adds null checks for `LayoutSet.Tasks`
danielskovli Sep 3, 2024
e36b6cb
Almost make the tests pass
ivarne Sep 4, 2024
df3fa25
Fix last remaining known bugs
ivarne Sep 4, 2024
552b846
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 4, 2024
eaf0af8
fix async warning
ivarne Sep 4, 2024
278e170
Don't filter fields to remove by doing a double lookup
ivarne Sep 4, 2024
ae2f6d9
Merge remote-tracking branch 'origin/feature/subform' into feat/multi…
ivarne Sep 4, 2024
269e0a3
Fix sonar suggestions and tests
ivarne Sep 4, 2024
c92d62b
fix another warning
ivarne Sep 4, 2024
46dafb4
Fix another batch of sonar cloud issues
ivarne Sep 4, 2024
7ea4e54
Implement SubFormComponent and use for validation
ivarne Sep 5, 2024
fae563f
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 5, 2024
dea4fe9
Fix tests after merge with main
ivarne Sep 5, 2024
85fc76c
Update storage.interfaces
ivarne Sep 6, 2024
6d3fb4e
Fix tests after storage upgrade
ivarne Sep 6, 2024
9a3a35a
Add LayoutId to required validation descriptions
ivarne Sep 6, 2024
1d30664
Add tests for subform validation
ivarne Sep 6, 2024
ef8dcab
Add appMetadata to LayoutEvaluatorState
ivarne Sep 7, 2024
110b8e8
Fix typos and make sonar more happy
ivarne Sep 7, 2024
dfb250a
Revert changes to use scoped services
ivarne Sep 8, 2024
f71dad1
Remove unused usings
ivarne Sep 8, 2024
4c6f4d0
Remove implicit cast, as it was no longer used in tests (and hid bugs…
ivarne Sep 8, 2024
225f934
Remove file added by error
ivarne Sep 9, 2024
e7f3413
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 9, 2024
82f859c
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 9, 2024
cc68b17
Add ignoredValidators as a comma separated GET parameter to /validate…
ivarne Sep 10, 2024
259212c
Cleanup validatorService. Add new tests and telemetry
ivarne Sep 10, 2024
b1b9085
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 10, 2024
1081d5f
Make sonar cloud complaints editor warnings instead of suggestions
ivarne Sep 10, 2024
18d4169
Activate CA1816: Call GC.SuppressFinalize correctly
ivarne Sep 10, 2024
93caa71
Fix more sonar cloud issues
ivarne Sep 10, 2024
54c5ffb
Make change to IProcessExclusiveGateway backwards compatible
ivarne Sep 10, 2024
450f53d
Add additional status codes to swagger
ivarne Sep 10, 2024
434528b
Update Instance.Data when writing copied data without shadow fields
ivarne Sep 11, 2024
32d5d15
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Sep 20, 2024
71b129f
Add concept of nonIncrementalValidators
ivarne Sep 23, 2024
f3ebc39
Add ModelSerializationService and other cleanup as a prerequisite to …
ivarne Sep 29, 2024
fdc4cb4
Add draft support for adding/removing/modifying other data elements i…
ivarne Sep 30, 2024
2e4cb52
Code review fixes
ivarne Sep 30, 2024
c77239f
Various cleanup tasks
ivarne Oct 3, 2024
29f2bb8
Rename DataElementId to DataElementIdentifier
ivarne Oct 3, 2024
1f75bb8
Merge pull request #809 from Altinn/feat/ivarne/multiDataProcessing
ivarne Oct 3, 2024
cc73fd4
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Oct 3, 2024
2ae6862
Fix issues and tests failing after merge
ivarne Oct 3, 2024
7ddd6d0
Rename id=>dataElementId in api
ivarne Oct 3, 2024
9e493c5
Merge remote-tracking branch 'origin/main' into feat/multi-data-model…
ivarne Oct 3, 2024
a6ca853
Use async versions of File access methods in DataClientMock
ivarne Oct 3, 2024
165af46
Reorganize ProcessTaskFinalizer to use CachedInstanceDataAccessor
ivarne Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ csharp_style_namespace_declarations = file_scoped:error
dotnet_diagnostic.IDE1006.severity = error

# Unused usings
dotnet_diagnostic.IDE0005.severity = suggestion
dotnet_diagnostic.IDE0005.severity = warning

# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = none
Expand All @@ -123,7 +123,7 @@ dotnet_diagnostic.CA1727.severity = suggestion
dotnet_diagnostic.CA2254.severity = none

# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1822.severity = warning

# IDE0080: Remove unnecessary suppression operator
dotnet_diagnostic.IDE0080.severity = error
Expand All @@ -146,6 +146,9 @@ dotnet_diagnostic.CA2201.severity = suggestion
# TODO: fixing this would be breaking
dotnet_diagnostic.CA1720.severity = suggestion

# CA1816: Call GC.SuppressFinalize correctly
dotnet_diagnostic.CA1816.severity = warning

[Program.cs]
dotnet_diagnostic.CA1050.severity = none
dotnet_diagnostic.S1118.severity = none
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,6 @@ src/Altinn.Apps/AppTemplates/AspNet/App/Properties/launchSettings.json
*.iml

.mono/

*.received.txt
*.recevied.json
2 changes: 1 addition & 1 deletion src/Altinn.App.Api/Altinn.App.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<ItemGroup>
<PackageReference Include="Altinn.Common.PEP" Version="4.0.0" />
<PackageReference Include="Altinn.Platform.Storage.Interface" Version="3.30.0"/>
<PackageReference Include="Altinn.Platform.Storage.Interface" Version="4.0.0" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="3.5.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
Expand Down
172 changes: 84 additions & 88 deletions src/Altinn.App.Api/Controllers/ActionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Action;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Helpers.Serialization;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Instances;
Expand Down Expand Up @@ -33,23 +33,19 @@ public class ActionsController : ControllerBase
private readonly IValidationService _validationService;
private readonly IDataClient _dataClient;
private readonly IAppMetadata _appMetadata;
private readonly ModelSerializationService _modelSerialization;

/// <summary>
/// Create new instance of the <see cref="ActionsController"/> class
/// </summary>
/// <param name="authorization">The authorization service</param>
/// <param name="instanceClient">The instance client</param>
/// <param name="userActionService">The user action service</param>
/// <param name="validationService">Service for performing validations of user data</param>
/// <param name="dataClient">Client for accessing data in storage</param>
/// <param name="appMetadata">Service for getting application metadata</param>
public ActionsController(
IAuthorizationService authorization,
IInstanceClient instanceClient,
UserActionService userActionService,
IValidationService validationService,
IDataClient dataClient,
IAppMetadata appMetadata
IAppMetadata appMetadata,
ModelSerializationService modelSerialization
)
{
_authorization = authorization;
Expand All @@ -58,12 +54,13 @@ IAppMetadata appMetadata
_validationService = validationService;
_dataClient = dataClient;
_appMetadata = appMetadata;
_modelSerialization = modelSerialization;
}

/// <summary>
/// Perform a task action on an instance
/// </summary>
/// <param name="org">unique identfier of the organisation responsible for the app</param>
/// <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="instanceOwnerPartyId">unique id of the party that this the owner of the instance</param>
/// <param name="instanceGuid">unique id to identify the instance</param>
Expand Down Expand Up @@ -129,8 +126,9 @@ public async Task<ActionResult<UserActionResponse>> Perform(
return Forbid();
}

var dataAccessor = new CachedInstanceDataAccessor(instance, _dataClient, _appMetadata, _modelSerialization);
UserActionContext userActionContext =
new(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata, language);
new(dataAccessor, userId.Value, actionRequest.ButtonId, actionRequest.Metadata, language);
IUserAction? actionHandler = _userActionService.GetActionHandler(action);
if (actionHandler == null)
{
Expand Down Expand Up @@ -162,105 +160,103 @@ public async Task<ActionResult<UserActionResponse>> Perform(
);
}

// If the action handler returns UpdatedDataModels, instead of using the dataMutator
// we need to update the dataAccessor with the new data in case it was fetched with DataClient
#pragma warning disable CS0618 // Type or member is obsolete
if (result.UpdatedDataModels is { Count: > 0 })
{
await SaveChangedModels(instance, result.UpdatedDataModels);
foreach (var (elementId, newModel) in result.UpdatedDataModels)
{
if (newModel is null)
{
continue;
}

var dataElement =
instance.Data.First(d => d.Id.Equals(elementId, StringComparison.OrdinalIgnoreCase))
?? throw new InvalidOperationException(
$"Action handler {actionHandler.GetType().Name} returned an updated data model for a data element that does not exist: {elementId}"
);

// update dataAccessor to use the changed data
dataAccessor.ReplaceFormDataAssumeSavedToStorage(dataElement, newModel);
Dismissed Show dismissed Hide dismissed
}
}
#pragma warning restore CS0618 // Type or member is obsolete

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

await dataAccessor.UpdateInstanceData();

var saveTask = dataAccessor.SaveChanges(changes);

var validationIssues = await GetIncrementalValidations(
instance,
dataAccessor,
changes,
actionRequest.IgnoredValidators,
language
);
await saveTask;

return new OkObjectResult(
return Ok(
new UserActionResponse()
{
ClientActions = result.ClientActions,
UpdatedDataModels = result.UpdatedDataModels,
UpdatedValidationIssues = await GetValidations(
instance,
result.UpdatedDataModels,
actionRequest.IgnoredValidators,
language
),
UpdatedDataModels = changes.ToDictionary(c => c.DataElement.Id, c => c.CurrentFormData),
UpdatedValidationIssues = validationIssues,
RedirectUrl = result.RedirectUrl,
}
);
}

private async Task SaveChangedModels(Instance instance, Dictionary<string, object> resultUpdatedDataModels)
{
var instanceIdentifier = new InstanceIdentifier(instance);
foreach (var (elementId, newModel) in resultUpdatedDataModels)
{
if (newModel is null)
{
continue;
}

ObjectUtils.InitializeAltinnRowId(newModel);
ObjectUtils.PrepareModelForXmlStorage(newModel);

var dataElement = instance.Data.First(d => d.Id.Equals(elementId, StringComparison.OrdinalIgnoreCase));
await _dataClient.UpdateData(
newModel,
instanceIdentifier.InstanceGuid,
newModel.GetType(),
instance.Org,
instance.AppId.Split('/')[1],
instanceIdentifier.InstanceOwnerPartyId,
Guid.Parse(dataElement.Id)
);
}
}

private async Task<Dictionary<string, Dictionary<string, List<ValidationIssue>>>?> GetValidations(
private async Task<Dictionary<
string,
Dictionary<string, List<ValidationIssueWithSource>>
>?> GetIncrementalValidations(
Instance instance,
Dictionary<string, object>? resultUpdatedDataModels,
IInstanceDataAccessor dataAccessor,
List<DataElementChange> changes,
List<string>? ignoredValidators,
string? language
)
{
if (resultUpdatedDataModels is null || resultUpdatedDataModels.Count < 1)
{
return null;
}

var instanceIdentifier = new InstanceIdentifier(instance);
var application = await _appMetadata.GetApplicationMetadata();
var taskId = instance.Process.CurrentTask.ElementId;
var validationIssues = await _validationService.ValidateIncrementalFormData(
instance,
dataAccessor,
taskId,
changes,
ignoredValidators,
language
);

var updatedValidationIssues = new Dictionary<string, Dictionary<string, List<ValidationIssue>>>();
// For historical reasons the validation issues from actions controller is separated per data element
// The easiest way was to keep this behaviour to improve compatibility with older frontend versions
return PartitionValidationIssuesByDataElement(validationIssues);
}

// TODO: Consider validating models in parallel
foreach (var (elementId, newModel) in resultUpdatedDataModels)
private static Dictionary<
string,
Dictionary<string, List<ValidationIssueWithSource>>
> PartitionValidationIssuesByDataElement(List<ValidationSourcePair> validationIssues)
{
var updatedValidationIssues = new Dictionary<string, Dictionary<string, List<ValidationIssueWithSource>>>();
foreach (var (validationSource, issuesFromSource) in validationIssues)
{
if (newModel is null)
foreach (var issue in issuesFromSource)
{
continue;
}

var dataElement = instance.Data.First(d => d.Id.Equals(elementId, StringComparison.OrdinalIgnoreCase));
var dataType = application.DataTypes.First(d =>
d.Id.Equals(dataElement.DataType, StringComparison.OrdinalIgnoreCase)
);

// TODO: Consider rewriting so that we get the original data the IUserAction have requested instead of fetching it again
var oldData = await _dataClient.GetFormData(
instanceIdentifier.InstanceGuid,
newModel.GetType(),
instance.Org,
instance.AppId.Split('/')[1],
instanceIdentifier.InstanceOwnerPartyId,
Guid.Parse(dataElement.Id)
);

var validationIssues = await _validationService.ValidateFormData(
instance,
dataElement,
dataType,
newModel,
oldData,
ignoredValidators,
language
);
if (validationIssues.Count > 0)
{
updatedValidationIssues.Add(elementId, validationIssues);
if (!updatedValidationIssues.TryGetValue(issue.DataElementId ?? "", out var elementIssues))
{
elementIssues = [];
updatedValidationIssues[issue.DataElementId ?? ""] = elementIssues;
}
if (!elementIssues.TryGetValue(validationSource, out var sourceIssues))
{
sourceIssues = [];
elementIssues[validationSource] = sourceIssues;
}
sourceIssues.Add(issue);
}
}

Expand Down
Loading
Loading