Skip to content

Commit

Permalink
Use item dynamic properties special handling for body parameter (#2823)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LucGenetier authored and anderson-joyle committed Jan 23, 2025
1 parent 1bf13e3 commit e6c6066
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 42 deletions.
25 changes: 17 additions & 8 deletions src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -1458,14 +1459,21 @@ private ConnectorParameterInternals Initialize()
foreach (KeyValuePair<string, OpenApiSchema> 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())
{
Expand Down Expand Up @@ -1611,7 +1619,8 @@ private ConnectorParameterInternals Initialize()
ContentType = contentType,
BodySchemaReferenceId = bodySchemaReferenceId,
ParameterDefaultValues = parameterDefaultValues,
SchemaLessBody = schemaLessBody
SchemaLessBody = schemaLessBody,
SpecialBodyHandling = specialBodyHandling
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,36 @@ 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);
}
}

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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,28 @@ public async Task<HttpRequestMessage> 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<string, string>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, (ISwaggerSchema, FormulaValue)> bodyParts = new ();
Dictionary<string, (ISwaggerSchema, FormulaValue)> bodyParts = new ();
Dictionary<string, FormulaValue> incomingParameters = ConvertToNamedParameters(args);
string contentType = null;

foreach (KeyValuePair<ConnectorParameter, FormulaValue> 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)
{
Expand Down Expand Up @@ -200,6 +213,7 @@ public Dictionary<string, FormulaValue> ConvertToNamedParameters(FormulaValue[]
// Parameter names are case sensitive.

Dictionary<string, FormulaValue> map = new ();
bool specialBodyHandling = _function._internals.SpecialBodyHandling;

// Seed with default values. This will get overwritten if provided.
foreach (KeyValuePair<string, (bool required, FormulaValue fValue, DType dType)> kv in _function._internals.ParameterDefaultValues)
Expand All @@ -217,9 +231,9 @@ public Dictionary<string, FormulaValue> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ internal class ConnectorParameterInternals
internal string BodySchemaReferenceId { get; init; }

internal Dictionary<string, (bool, FormulaValue, DType)> ParameterDefaultValues { get; init; }

internal bool SpecialBodyHandling { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public bool ReturnEnumsAsPrimitive { get; init; } = false;

public bool ReturnEnumsAsPrimitive { get; init; } = false;

/// <summary>
/// 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.
/// </summary>
public bool UseDefaultBodyNameForSinglePropertyObject { get; init; } = false;

public bool UseItemDynamicPropertiesSpecialHandling { get; init; } = false;
public ConnectorCompatibility Compatibility { get; init; } = ConnectorCompatibility.Default;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectorFunction> 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<object>(expected, testConnector._log.ToString());
}

public class HttpLogger : HttpClient
Expand Down

0 comments on commit e6c6066

Please sign in to comment.