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

Multipart form data #329

Merged
merged 24 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
236f553
Let ModelDeserializer return a Result type instead of nullable
ivarne Oct 20, 2023
d005192
Remove NullDataProcessor and let user registrer multiple processors
ivarne Oct 24, 2023
9a87e56
Update IDataProcessor interface and support multipart data write
ivarne Oct 25, 2023
c4e456d
Update v7tov8 script for new IDataProcessor interface
ivarne Oct 25, 2023
9c2ca7d
Code review updates
ivarne Nov 15, 2023
6d66fd3
Update src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs
ivarne Nov 15, 2023
5f43aac
First draft of tests for controllers
ivarne Nov 24, 2023
edd91bd
Finish writing tests
ivarne Nov 27, 2023
f4f3590
Set partyId in token to null
ivarne Nov 28, 2023
a52e601
Fix bad case for roles folder in testsetup
tjololo Nov 28, 2023
a8ef92e
Add partyId correctly in test tokens
ivarne Nov 28, 2023
5820855
Merge remote-tracking branch 'origin/v8' into ivarne/multipart-form-data
ivarne Nov 30, 2023
a2cb36f
Use proper parsing library to decode multipart/form-data requests
ivarne Nov 30, 2023
d5e3736
Continue returning changed only changed values from data put.
ivarne Nov 30, 2023
6d663fb
Fix so that it compiles (still failing tests)
ivarne Nov 30, 2023
d29d4cc
Fix tests by disabeling redirects in applciationfactory client
ivarne Dec 2, 2023
fa85643
Add more tests
ivarne Dec 4, 2023
d22dfac
More tests
ivarne Dec 4, 2023
66ad588
Update src/Altinn.App.Api/Controllers/DataController.cs
ivarne Dec 6, 2023
df2d7b1
Fix code smells
ivarne Dec 8, 2023
28e66fa
Fix tests
ivarne Dec 8, 2023
fa06880
Tests OK now?
ivarne Dec 8, 2023
d2cd711
Merge remote-tracking branch 'origin/v8' into ivarne/multipart-form-data
ivarne Dec 8, 2023
3889386
More tests for model deserializer
ivarne Dec 8, 2023
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
10 changes: 9 additions & 1 deletion cli-tools/altinn-app-cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.CommandLine;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Reflection;
using altinn_app_cli.v7Tov8.AppSettingsRewriter;
Expand Down Expand Up @@ -180,12 +180,20 @@ static async Task<int> UpgradeCode(string projectFile)
{
await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString());
}

UsingRewriter usingRewriter = new();
var newUsingSource = usingRewriter.Visit(newSource);
if (newUsingSource != newSource)
{
await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString());
}

DataProcessorRewriter dataProcessorRewriter = new(sm);
var dataProcessorSource = dataProcessorRewriter.Visit(newUsingSource);
if (dataProcessorSource != newUsingSource)
{
await File.WriteAllTextAsync(sourceTree.FilePath, dataProcessorSource.ToFullString());
}
}

Console.WriteLine("References and using upgraded");
Expand Down
4 changes: 2 additions & 2 deletions cli-tools/altinn-app-cli/altinn-app-cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>altinn_app_cli</RootNamespace>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down Expand Up @@ -31,7 +31,7 @@
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerTagPrefix>altinn-app-cli</MinVerTagPrefix>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>10.0</LangVersion>
<LangVersion>12</LangVersion>
</PropertyGroup>

<Target Name="AssemblyVersionTarget" AfterTargets="MinVer" Condition="'$(MinVerVersion)'!=''">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace altinn_app_cli.v7Tov8.CodeRewriters
{
public class DataProcessorRewriter : CSharpSyntaxRewriter
{
private readonly SemanticModel semanticModel;

public DataProcessorRewriter(SemanticModel semanticModel)
{
this.semanticModel = semanticModel;
}

public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node)
{
// Ignore any classes that don't implement `IDataProcessor` (consider using semantic model to ensure correct reference)
if (node.BaseList?.Types.Any(t => t.Type.ToString() == "IDataProcessor") == true)
{
var processDataWrite = node.Members.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataWrite");
if (processDataWrite is not null)
{
node = node.ReplaceNode(processDataWrite, Update_DataProcessWrite(processDataWrite));
}

var processDataRead = node.Members.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataRead");
if (processDataRead is not null)
{
node = node.ReplaceNode(processDataRead, Update_DataProcessRead(processDataRead));
}
}

return base.VisitClassDeclaration(node);
}

private MethodDeclarationSyntax Update_DataProcessRead(MethodDeclarationSyntax processDataRead)
{
if (processDataRead.ParameterList.Parameters.Count == 3 &&
processDataRead.ReturnType.ToString() == "Task<bool>")
{
processDataRead = ChangeReturnType_FromTaskBool_ToTask(processDataRead);
}

return processDataRead;
}

private MethodDeclarationSyntax Update_DataProcessWrite(MethodDeclarationSyntax processDataWrite)
{
if (processDataWrite.ParameterList.Parameters.Count == 3 &&
processDataWrite.ReturnType.ToString() == "Task<bool>")
{
processDataWrite = AddParameter_ChangedFields(processDataWrite);
processDataWrite = ChangeReturnType_FromTaskBool_ToTask(processDataWrite);
}

return processDataWrite;
}

private MethodDeclarationSyntax AddParameter_ChangedFields(MethodDeclarationSyntax method)
{
return method.ReplaceNode(method.ParameterList,
method.ParameterList.AddParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("changedFields"))
.WithLeadingTrivia(SyntaxFactory.Space)
.WithType(SyntaxFactory.ParseTypeName("System.Collections.Generic.Dictionary<string, string?>?"))
.WithLeadingTrivia(SyntaxFactory.Space)));
}

private MethodDeclarationSyntax ChangeReturnType_FromTaskBool_ToTask(MethodDeclarationSyntax method)
{
if (method.ReturnType.ToString() == "Task<bool>")
{
var returnTypeRewriter = new ReturnTypeTaskBooleanRewriter();
method = (MethodDeclarationSyntax)returnTypeRewriter.Visit(method)!;
}

return method;

}
}

public class ReturnTypeTaskBooleanRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
{
if (node.ReturnType.ToString() == "Task<bool>")
{
// Change return type
node = node.WithReturnType(
SyntaxFactory.ParseTypeName("Task").WithTrailingTrivia(SyntaxFactory.Space));
}
return base.VisitMethodDeclaration(node);
}

public override SyntaxNode? VisitBlock(BlockSyntax node)
{
foreach (var returnStatementSyntax in node.Statements.OfType<ReturnStatementSyntax>())
{
var leadingTrivia = returnStatementSyntax.GetLeadingTrivia();
var trailingTrivia = returnStatementSyntax.GetTrailingTrivia();
// When we add multiple lines of code, we need the indentation and a newline
var leadingTriviaMiddle = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia));
var trailingTriviaMiddle = trailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia));
// If we don't find a newline, just guess that LF is used. Will likely work anyway.
if (trailingTriviaMiddle == default) trailingTriviaMiddle = SyntaxFactory.LineFeed;


switch (returnStatementSyntax.Expression)
{
// return true/false/variableName
case IdentifierNameSyntax:
case LiteralExpressionSyntax:
case null:
node = node.ReplaceNode(returnStatementSyntax,
SyntaxFactory.ReturnStatement()
.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia));
break;
// case "Task.FromResult(...)":
case InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } },
Name: { Identifier: {Text: "FromResult"}}
},
ArgumentList: { Arguments: { Count: 1 } }
}:
node = node.ReplaceNode(returnStatementSyntax,
SyntaxFactory.ReturnStatement(SyntaxFactory.ParseExpression(" Task.CompletedTask"))
.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia));
break;
// case "await Task.FromResult(...)":
// Assume we need an await to silence CS1998 and rewrite to
// await Task.CompletedTask; return;
// Could be dropped if we ignore CS1998
case AwaitExpressionSyntax
{
Expression: InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } },
Name: { Identifier: {Text: "FromResult"}}
},
ArgumentList: { Arguments: [{Expression: IdentifierNameSyntax or LiteralExpressionSyntax}]}
}
}:
node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax, new StatementSyntax[]
{
// Uncomment if cs1998 isn't disabled
// SyntaxFactory.ParseStatement("await Task.CompletedTask;")
// .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle),

SyntaxFactory.ReturnStatement()
.WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia),

}));
break;
// Just add move the return; statement after the existing return value
default:
node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax,
new StatementSyntax[]
{
SyntaxFactory.ExpressionStatement(returnStatementSyntax.Expression)
.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle),

SyntaxFactory.ReturnStatement()
.WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia),
}));
break;
}
}

return base.VisitBlock(node);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,36 @@ public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0
public async Task Upgrade()
{
var altinnAppCoreElements = GetAltinnAppCoreElement();
altinnAppCoreElements?.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion));

var altinnAppApiElements = GetAltinnAppApiElement();
if (altinnAppCoreElements != null && altinnAppApiElements != null)
altinnAppApiElements?.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion));

IgnoreWarnings("1591", "1998"); // Require xml doc and await in async methods

await Save();
}

private void IgnoreWarnings(params string[] warnings)
{
var noWarn = doc.Root?.Elements("PropertyGroup").Elements("NoWarn").ToList();
switch (noWarn?.Count)
{
altinnAppCoreElements.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion));
altinnAppApiElements.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion));
await Save();
case 0:
doc.Root?.Elements("PropertyGroup").First().Add(new XElement("NoWarn", "$(NoWarn);" + string.Join(';', warnings)));
break;

case 1:
var valueElement = noWarn.First();
foreach (var warning in warnings)
{
if (!valueElement.Value.Contains(warning))
{
valueElement.SetValue($"{valueElement.Value};{warning}");
}
}

break;
}
}

Expand Down
27 changes: 11 additions & 16 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class DataController : ControllerBase
{
private readonly ILogger<DataController> _logger;
private readonly IDataClient _dataClient;
private readonly IDataProcessor _dataProcessor;
private readonly IEnumerable<IDataProcessor> _dataProcessors;
private readonly IInstanceClient _instanceClient;
private readonly IInstantiationProcessor _instantiationProcessor;
private readonly IAppModel _appModel;
Expand All @@ -58,7 +58,7 @@ public class DataController : ControllerBase
/// <param name="instanceClient">instance service to store instances</param>
/// <param name="instantiationProcessor">Instantiation processor</param>
/// <param name="dataClient">A service with access to data storage.</param>
/// <param name="dataProcessor">Serive implemnting logic during data read/write</param>
/// <param name="dataProcessors">Serive implemnting logic during data read/write</param>
ivarne marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="appModel">Service for generating app model</param>
/// <param name="appResourcesService">The apps resource service</param>
/// <param name="appMetadata">The app metadata service</param>
Expand All @@ -71,7 +71,7 @@ public DataController(
IInstanceClient instanceClient,
IInstantiationProcessor instantiationProcessor,
IDataClient dataClient,
IDataProcessor dataProcessor,
IEnumerable<IDataProcessor> dataProcessors,
IAppModel appModel,
IAppResources appResourcesService,
IPrefill prefillService,
Expand All @@ -85,7 +85,7 @@ public DataController(
_instanceClient = instanceClient;
_instantiationProcessor = instantiationProcessor;
_dataClient = dataClient;
_dataProcessor = dataProcessor;
_dataProcessors = dataProcessors;
_appModel = appModel;
_appResourcesService = appResourcesService;
_appMetadata = appMetadata;
Expand Down Expand Up @@ -175,7 +175,7 @@ public async Task<ActionResult> Create(
_logger.LogError(errorMessage);
return BadRequest(await GetErrorDetails(new List<ValidationIssue> { error }));
}

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

Expand Down Expand Up @@ -584,7 +584,11 @@ private async Task<ActionResult> GetFormData(
return BadRequest($"Did not find form data for data element {dataGuid}");
}

await _dataProcessor.ProcessDataRead(instance, dataGuid, appModel);
foreach (var dataProcessor in _dataProcessors)
{
_logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name);
await dataProcessor.ProcessDataRead(instance, dataGuid, appModel);
}

string? userOrgClaim = User.GetOrg();
if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase))
Expand Down Expand Up @@ -625,16 +629,7 @@ private async Task<ActionResult> PutFormData(string org, string app, Instance in
return BadRequest(deserializerResult.Error);
}

Dictionary<string, object?>? changedFields = null;
if (deserializerResult.ReportedChanges is not null)
{
//TODO: call new and old dataProcessors
changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessor, _logger);
}
else
{
changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessor, _logger);
}
Dictionary<string, object?>? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessors, deserializerResult.ReportedChanges, _logger);

await UpdatePresentationTextsOnInstance(instance, dataType, deserializerResult.Model);
await UpdateDataValuesOnInstance(instance, dataType, deserializerResult.Model);
Expand Down
Loading
Loading