From e6c6066488c0fcaa1ee9c1a2ab0cc3681a31f737 Mon Sep 17 00:00:00 2001 From: Luc Genetier <69138830+LucGenetier@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:28:21 +0100 Subject: [PATCH] Use item dynamic properties special handling for body parameter (#2823) Add new connector setting _UseItemDynamicPropertiesSpecialHandling_ Remove _UseDefaultBodyNameForSinglePropertyObject_ as it was not fixing the issue as expected Special case handling when - body name is 'item' - body inner object is 'dynamicProperties' - there is only one property in inner object In that base the body will be fully flattened and we will retain the 'body' name for the parameter. --- .../ConnectorFunction.cs | 25 +++-- .../Execution/FormulaValueSerializer.cs | 39 ++++---- .../Execution/HttpFunctionInvoker.cs | 24 ++++- .../Internal/ConnectorParameterInternals.cs | 2 + .../Public/ConnectorSettings.cs | 17 ++-- .../PowerPlatformConnectorTests.cs | 93 ++++++++++++++++++- 6 files changed, 158 insertions(+), 42 deletions(-) diff --git a/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs b/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs index ee49cf5002..522c8b8017 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs @@ -1373,8 +1373,9 @@ private ConnectorParameterInternals Initialize() string bodySchemaReferenceId = null; bool schemaLessBody = false; bool fatalError = false; + bool specialBodyHandling = false; string contentType = OpenApiExtensions.ContentType_ApplicationJson; - ConnectorErrors errorsAndWarnings = new ConnectorErrors(); + ConnectorErrors errorsAndWarnings = new ConnectorErrors(); foreach (OpenApiParameter parameter in Operation.Parameters) { @@ -1458,14 +1459,21 @@ private ConnectorParameterInternals Initialize() foreach (KeyValuePair bodyProperty in bodySchema.Properties) { OpenApiSchema bodyPropertySchema = bodyProperty.Value; - string bodyPropertyName = bodyProperty.Key; - bool bodyPropertyRequired = bodySchema.Required.Contains(bodyPropertyName); - bool bodyPropertyHiddenRequired = false; - - if (ConnectorSettings.UseDefaultBodyNameForSinglePropertyObject && bodySchema.Properties.Count == 1) + string bodyPropertyName = bodyProperty.Key; + bool bodyPropertyHiddenRequired = false; + + // Power Apps has a special handling for the body in this case + // where it doesn't follow the swagger file + if (ConnectorSettings.UseItemDynamicPropertiesSpecialHandling && + bodyName == "item" && + bodyPropertyName == "dynamicProperties" && + bodySchema.Properties.Count == 1) { bodyPropertyName = bodyName; - } + specialBodyHandling = true; + } + + bool bodyPropertyRequired = bodySchema.Required.Contains(bodyPropertyName) || (ConnectorSettings.UseItemDynamicPropertiesSpecialHandling && requestBody.Required); if (bodyPropertySchema.IsInternal()) { @@ -1611,7 +1619,8 @@ private ConnectorParameterInternals Initialize() ContentType = contentType, BodySchemaReferenceId = bodySchemaReferenceId, ParameterDefaultValues = parameterDefaultValues, - SchemaLessBody = schemaLessBody + SchemaLessBody = schemaLessBody, + SpecialBodyHandling = specialBodyHandling }; } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Execution/FormulaValueSerializer.cs b/src/libraries/Microsoft.PowerFx.Connectors/Execution/FormulaValueSerializer.cs index 048d6ed780..d1da3872f4 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Execution/FormulaValueSerializer.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Execution/FormulaValueSerializer.cs @@ -108,21 +108,8 @@ private async Task WriteObjectAsync(string objectName, ISwaggerSchema schema, IE await WritePropertyAsync( nv.Name, new SwaggerSchema( - type: nv.Value.Type._type.Kind switch - { - DKind.Number => "number", - DKind.Decimal => "number", - DKind.String or - DKind.Date or - DKind.DateTime or - DKind.DateTimeNoTimeZone => "string", - DKind.Boolean => "boolean", - DKind.Record => "object", - DKind.Table => "array", - DKind.ObjNull => "null", - _ => $"type: unknown_dkind {nv.Value.Type._type.Kind}" - }, - format: GetDateFormat(nv.Value.Type._type.Kind)), + type: GetType(nv.Value.Type), + format: GetFormat(nv.Value.Type)), nv.Value).ConfigureAwait(false); } } @@ -130,9 +117,27 @@ DKind.DateTime or EndObject(objectName); } - private static string GetDateFormat(DKind kind) + internal static string GetType(FormulaType type) { - return kind switch + return type._type.Kind switch + { + DKind.Number => "number", + DKind.Decimal => "number", + DKind.String or + DKind.Date or + DKind.DateTime or + DKind.DateTimeNoTimeZone => "string", + DKind.Boolean => "boolean", + DKind.Record => "object", + DKind.Table => "array", + DKind.ObjNull => "null", + _ => $"type: unknown_dkind {type._type.Kind}" + }; + } + + internal static string GetFormat(FormulaType type) + { + return type._type.Kind switch { DKind.Date => "date", DKind.DateTime => "date-time", diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Execution/HttpFunctionInvoker.cs b/src/libraries/Microsoft.PowerFx.Connectors/Execution/HttpFunctionInvoker.cs index 1a01418490..b304ce4f19 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Execution/HttpFunctionInvoker.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Execution/HttpFunctionInvoker.cs @@ -57,15 +57,28 @@ public async Task BuildRequest(FormulaValue[] args, IConvert // Header names are not case sensitive. // From RFC 2616 - "Hypertext Transfer Protocol -- HTTP/1.1", Section 4.2, "Message Headers" var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary bodyParts = new (); + Dictionary bodyParts = new (); Dictionary incomingParameters = ConvertToNamedParameters(args); string contentType = null; foreach (KeyValuePair param in _function._internals.OpenApiBodyParameters) - { + { if (incomingParameters.TryGetValue(param.Key.Name, out var paramValue)) { - bodyParts.Add(param.Key.Name, (param.Key.Schema, paramValue)); + if (_function._internals.SpecialBodyHandling && paramValue is RecordValue rv) + { + foreach (NamedValue field in rv.Fields) + { + string type = FormulaValueSerializer.GetType(field.Value.Type); + string format = FormulaValueSerializer.GetFormat(field.Value.Type); + + bodyParts.Add(field.Name, (new SwaggerSchema(type, format), field.Value)); + } + } + else + { + bodyParts.Add(param.Key.Name, (param.Key.Schema, paramValue)); + } } else if (param.Key.Schema.Default != null && param.Value != null) { @@ -200,6 +213,7 @@ public Dictionary ConvertToNamedParameters(FormulaValue[] // Parameter names are case sensitive. Dictionary map = new (); + bool specialBodyHandling = _function._internals.SpecialBodyHandling; // Seed with default values. This will get overwritten if provided. foreach (KeyValuePair kv in _function._internals.ParameterDefaultValues) @@ -217,9 +231,9 @@ public Dictionary ConvertToNamedParameters(FormulaValue[] { string parameterName = _function.RequiredParameters[i].Name; FormulaValue paramValue = args[i]; - + // Objects are always flattenned - if (paramValue is RecordValue record && !_function.RequiredParameters[i].IsBodyParameter) + if (paramValue is RecordValue record && (specialBodyHandling || !_function.RequiredParameters[i].IsBodyParameter)) { foreach (NamedValue field in record.Fields) { diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Internal/ConnectorParameterInternals.cs b/src/libraries/Microsoft.PowerFx.Connectors/Internal/ConnectorParameterInternals.cs index 5d5ea43c14..49d8471ed5 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Internal/ConnectorParameterInternals.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Internal/ConnectorParameterInternals.cs @@ -20,5 +20,7 @@ internal class ConnectorParameterInternals internal string BodySchemaReferenceId { get; init; } internal Dictionary ParameterDefaultValues { get; init; } + + internal bool SpecialBodyHandling { get; init; } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorSettings.cs b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorSettings.cs index 82431b299a..87b360205e 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorSettings.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorSettings.cs @@ -92,16 +92,17 @@ public bool ExposeInternalParamsWithoutDefaultValue /// This flag will force all enums to be returns as FormulaType.String or FormulaType.Decimal regardless of x-ms-enum-*. /// This flag is only in effect when SupportXMsEnumValues is true. /// - public bool ReturnEnumsAsPrimitive { get; init; } = false; - + public bool ReturnEnumsAsPrimitive { get; init; } = false; + /// - /// In Power Apps, when a body parameter is used it's flattened and we create one parameter for each - /// body object property. With that logic each parameter name will be the object property name. - /// When set, this setting will use the real body name specified in the swagger instead of the property name - /// of the object, provided there is only one property. + /// This flag enables some special handling for the body parameter, when + /// - body name is 'item' + /// - body inner object is 'dynamicProperties' + /// - there is only one property in inner object + /// In that base the body will be fully flattened and we will retain the 'body' name for the parameter. /// - public bool UseDefaultBodyNameForSinglePropertyObject { get; init; } = false; - + public bool UseItemDynamicPropertiesSpecialHandling { get; init; } = false; + public ConnectorCompatibility Compatibility { get; init; } = ConnectorCompatibility.Default; } diff --git a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs index 77c7cf72fe..4a7cabe87e 100644 --- a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs +++ b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs @@ -2402,20 +2402,105 @@ public async Task SQL_ExecuteStoredProc_Scoped() [Theory] [InlineData(true)] [InlineData(false)] - public void ExchangeOnlineTest2(bool useDefaultBodyNameForSinglePropertyObject) + public async Task ExchangeOnlineTest2(bool useItemDynamicPropertiesSpecialHandling) { + bool live = false; using var testConnector = new LoggingTestServer(@"Swagger\ExcelOnlineBusiness.swagger.json", _output); List functions = OpenApiParser.GetFunctions( - new ConnectorSettings("Excel") + new ConnectorSettings("ExcelOnline") { Compatibility = ConnectorCompatibility.Default, - UseDefaultBodyNameForSinglePropertyObject = useDefaultBodyNameForSinglePropertyObject + UseItemDynamicPropertiesSpecialHandling = useItemDynamicPropertiesSpecialHandling }, testConnector._apiDocument).ToList(); ConnectorFunction patchItem = functions.First(f => f.Name == "PatchItem"); + ConnectorParameter itemparam = useItemDynamicPropertiesSpecialHandling ? patchItem.RequiredParameters[6] : patchItem.OptionalParameters[2]; - Assert.Equal(!useDefaultBodyNameForSinglePropertyObject ? "dynamicProperties" : "item", patchItem.OptionalParameters[2].Name); + Assert.Equal(!useItemDynamicPropertiesSpecialHandling ? "dynamicProperties" : "item", itemparam.Name); + + FormulaValue[] parameters = new FormulaValue[7]; + parameters[0] = FormulaValue.New("b!IbvdIRe4LEGypNQpzV_eHMlG3PtubVREtOzk7doKeFvkIs8VRqloT4mtkIOb6aTB"); + parameters[1] = FormulaValue.New("013DZ3QDGY2Y23HOQN5BC2HUMJWD7G4UPL"); + parameters[2] = FormulaValue.New("{E5A21CC6-3B17-48DE-84D7-0326A06B38F4}"); + parameters[3] = FormulaValue.New("035fd7a2-34d6-4a6f-a885-a646b1398012"); + parameters[4] = FormulaValue.New("me"); + parameters[5] = FormulaValue.New("__PowerAppsId__"); + + parameters[6] = useItemDynamicPropertiesSpecialHandling + + ? // Required parameter + RecordValue.NewRecordFromFields( + new NamedValue("item", RecordValue.NewRecordFromFields( + new NamedValue("Column1", FormulaValue.New(171))))) + + : // Optional parameters + RecordValue.NewRecordFromFields( + new NamedValue("dynamicProperties", RecordValue.NewRecordFromFields( + new NamedValue("Column1", FormulaValue.New(171))))); + + using var httpClient = live ? new HttpClient() : new HttpClient(testConnector); + + if (!live) + { + string output = @"{ + ""@odata.context"": ""https://excelonline-wcus.azconn-wcus-001.p.azurewebsites.net/$metadata#drives('b%21IbvdIRe4LEGypNQpzV_eHMlG3PtubVREtOzk7doKeFvkIs8VRqloT4mtkIOb6aTB')/Files('013DZ3QDGY2Y23HOQN5BC2HUMJWD7G4UPL')/Tables('%7BE5A21CC6-3B17-48DE-84D7-0326A06B38F4%7D')/items/$entity"", + ""@odata.etag"": """", + ""ItemInternalId"": ""035fd7a2-34d6-4a6f-a885-a646b1398012"", + ""Column1"": ""171"", + ""Column2"": ""Customer1"", + ""Column3"": """", + ""__PowerAppsId__"": ""035fd7a2-34d6-4a6f-a885-a646b1398012"" +}"; + testConnector.SetResponse(output, HttpStatusCode.OK); + } + + string jwt = "eyJ0e..."; + using PowerPlatformConnectorClient client = new PowerPlatformConnectorClient("https://49970107-0806-e5a7-be5e-7c60e2750f01.12.common.firstrelease.azure-apihub.net", "49970107-0806-e5a7-be5e-7c60e2750f01", "e24a1ac719284479a4817a0c5bb6ef58", () => jwt, httpClient) + { + SessionId = "a41bd03b-6c3c-4509-a844-e8c51b61f878", + }; + + BaseRuntimeConnectorContext context = new TestConnectorRuntimeContext("ExcelOnline", client, console: _output); + FormulaValue result = await patchItem.InvokeAsync(parameters, context, CancellationToken.None); + + // Can't test the result as it's ![] and is an empty RecordValue + + if (live) + { + return; + } + + string version = PowerPlatformConnectorClient.Version; + string expected = useItemDynamicPropertiesSpecialHandling + ? $@"POST https://49970107-0806-e5a7-be5e-7c60e2750f01.12.common.firstrelease.azure-apihub.net/invoke + authority: 49970107-0806-e5a7-be5e-7c60e2750f01.12.common.firstrelease.azure-apihub.net + Authorization: Bearer {jwt} + path: /invoke + scheme: https + x-ms-client-environment-id: /providers/Microsoft.PowerApps/environments/49970107-0806-e5a7-be5e-7c60e2750f01 + x-ms-client-session-id: a41bd03b-6c3c-4509-a844-e8c51b61f878 + x-ms-request-method: PATCH + x-ms-request-url: /apim/excelonlinebusiness/e24a1ac719284479a4817a0c5bb6ef58/drives/b%21IbvdIRe4LEGypNQpzV_eHMlG3PtubVREtOzk7doKeFvkIs8VRqloT4mtkIOb6aTB/files/013DZ3QDGY2Y23HOQN5BC2HUMJWD7G4UPL/tables/%7BE5A21CC6-3B17-48DE-84D7-0326A06B38F4%7D/items/035fd7a2-34d6-4a6f-a885-a646b1398012?source=me&idColumn=__PowerAppsId__ + x-ms-user-agent: PowerFx/{version} + [content-header] Content-Type: application/json; charset=utf-8 + [body] {{""Column1"":171}} +" + : $@"POST https://49970107-0806-e5a7-be5e-7c60e2750f01.12.common.firstrelease.azure-apihub.net/invoke + authority: 49970107-0806-e5a7-be5e-7c60e2750f01.12.common.firstrelease.azure-apihub.net + Authorization: Bearer {jwt} + path: /invoke + scheme: https + x-ms-client-environment-id: /providers/Microsoft.PowerApps/environments/49970107-0806-e5a7-be5e-7c60e2750f01 + x-ms-client-session-id: a41bd03b-6c3c-4509-a844-e8c51b61f878 + x-ms-request-method: PATCH + x-ms-request-url: /apim/excelonlinebusiness/e24a1ac719284479a4817a0c5bb6ef58/drives/b%21IbvdIRe4LEGypNQpzV_eHMlG3PtubVREtOzk7doKeFvkIs8VRqloT4mtkIOb6aTB/files/013DZ3QDGY2Y23HOQN5BC2HUMJWD7G4UPL/tables/%7BE5A21CC6-3B17-48DE-84D7-0326A06B38F4%7D/items/035fd7a2-34d6-4a6f-a885-a646b1398012?source=me&idColumn=__PowerAppsId__ + x-ms-user-agent: PowerFx/{version} + [content-header] Content-Type: application/json; charset=utf-8 + [body] {{""dynamicProperties"":{{""Column1"":171}}}} +"; + + Assert.Equal(expected, testConnector._log.ToString()); } public class HttpLogger : HttpClient