From 8956f568c3747c5bda575133fb24cbc8ec3d25ea Mon Sep 17 00:00:00 2001 From: Luc Genetier <69138830+LucGenetier@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:19:46 +0100 Subject: [PATCH] Expose SpecialBodyHandling (#2828) --- .../ConnectorFunction.cs | 3217 +++++++++-------- .../PowerPlatformConnectorTests.cs | 1 + 2 files changed, 1613 insertions(+), 1605 deletions(-) diff --git a/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs b/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs index 522c8b8017..964ea37369 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs @@ -1,1023 +1,1031 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Interfaces; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Readers; -using Microsoft.OpenApi.Validations; -using Microsoft.PowerFx.Connectors.Localization; -using Microsoft.PowerFx.Core.Entities; -using Microsoft.PowerFx.Core.Errors; -using Microsoft.PowerFx.Core.Localization; -using Microsoft.PowerFx.Core.Types; -using Microsoft.PowerFx.Core.Types.Enums; -using Microsoft.PowerFx.Core.Utils; -using Microsoft.PowerFx.Functions; -using Microsoft.PowerFx.Intellisense; -using Microsoft.PowerFx.Types; -using static Microsoft.PowerFx.Connectors.ConnectorHelperFunctions; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Validations; +using Microsoft.PowerFx.Connectors.Localization; +using Microsoft.PowerFx.Core.Entities; +using Microsoft.PowerFx.Core.Errors; +using Microsoft.PowerFx.Core.Localization; +using Microsoft.PowerFx.Core.Types; +using Microsoft.PowerFx.Core.Types.Enums; +using Microsoft.PowerFx.Core.Utils; +using Microsoft.PowerFx.Functions; +using Microsoft.PowerFx.Intellisense; +using Microsoft.PowerFx.Types; +using static Microsoft.PowerFx.Connectors.ConnectorHelperFunctions; #pragma warning disable SA1117 - -namespace Microsoft.PowerFx.Connectors -{ - /// - /// Represents a connector function. - /// - [DebuggerDisplay("{Name}")] - public class ConnectorFunction - { - /// - /// Normalized name of the function. - /// - public string Name { get; } - - /// - /// Namespace of the function (not contained in swagger file). - /// - public string Namespace => ConnectorSettings.Namespace; - - /// - /// ConnectorSettings. Contains the Namespace and other parameters. - /// - public ConnectorSettings ConnectorSettings { get; } - - /// - /// Defines if the function is supported or contains unsupported elements. - /// - public bool IsSupported - { - get - { - EnsureInitialized(); - return _isSupported; - } - } - - /// - /// Reason for which the function isn't supported. - /// - public string NotSupportedReason - { - get - { - EnsureInitialized(); - return _notSupportedReason; - } - } - - /// - /// Warnings to be reported to end user. - /// - public IReadOnlyCollection Warnings - { - get - { - EnsureInitialized(); - return _warnings; - } - } - - /// - /// Defines if the function is deprecated. - /// - public bool IsDeprecated => Operation.Deprecated; - - /// - /// Page Link as defined in the x-ms-pageable extension. - /// This is the name of the property that will host the URL, not the URL itself. - /// - public string PageLink => Operation.PageLink(); - - /// - /// Name as it appears in the swagger file. - /// - public string OriginalName => Operation.OperationId; - - /// - /// Operation's description. - /// - public string Description => Operation.Description ?? $"Invoke {Name}"; - - /// - /// Operation's summary. - /// - public string Summary => Operation.Summary; - - /// - /// Operation's path. Includes the base path if any. - /// - public string OperationPath { get; } - - /// - /// HTTP method. - /// - public HttpMethod HttpMethod { get; } - - /// - /// Defines behavioral functions. - /// HTTP/1.1 spec states that only GET and HEAD requests are 'safe' by default. - /// https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html. - /// - public bool IsBehavior => HttpMethod != HttpMethod.Get && HttpMethod != HttpMethod.Head; - - /// - /// Defines if the function is pageable (using x-ms-pageable extension). - /// - public bool IsPageable => !string.IsNullOrEmpty(PageLink); - - /// - /// Visibility defined as "x-ms-visibility" string content. - /// - public string Visibility => Operation.GetVisibility(); - - /// - /// When "x-ms-visibility" is set to "internal". - /// https://learn.microsoft.com/en-us/connectors/custom-connectors/openapi-extensions#x-ms-visibility. - /// - public bool IsInternal => Operation.IsInternal(); - - /// - /// Defined as "x-ms-require-user-confirmation" boolean content. - /// - public bool RequiresUserConfirmation => Operation.GetRequiresUserConfirmation(); - - /// - /// Return type of the function. - /// - public FormulaType ReturnType => Operation.GetReturnType(ConnectorSettings); - - /// - /// Minimum number of arguments. - /// - public int ArityMin - { - get - { - EnsureInitialized(); - return _arityMin; - } - } - - /// - /// Maximum number of arguments. - /// - public int ArityMax - { - get - { - EnsureInitialized(); - return _arityMax; - } - } - - /// - /// Required parameters. - /// - public ConnectorParameter[] RequiredParameters - { - get - { - EnsureInitialized(); - return _requiredParameters; - } - } - - /// - /// Optional parameters. - /// - public ConnectorParameter[] OptionalParameters - { - get - { - EnsureInitialized(); - return _optionalParameters; - } - } - - /// - /// Swagger's operation. - /// - internal OpenApiOperation Operation { get; } - - /// - /// Hidden required parameters. - /// Defined as - /// - part "required" swagger array - /// - "x-ms-visibility" string set to "internal" - /// - has a default value. - /// - internal ConnectorParameter[] HiddenRequiredParameters - { - get - { - EnsureInitialized(); - return _hiddenRequiredParameters; - } - } - - /// - /// OpenApiServers defined in the document. - /// - internal IEnumerable Servers { get; init; } - - /// - /// Filtered function. - /// - internal bool Filtered { get; } - - /// - /// Dynamic schema extension on return type (response). - /// - internal ConnectorDynamicSchema DynamicReturnSchema => EnsureConnectorFunction(ReturnParameterType?.DynamicSchema, GlobalContext.FunctionList); - - /// - /// Dynamic schema extension on return type (response). - /// - internal ConnectorDynamicProperty DynamicReturnProperty => EnsureConnectorFunction(ReturnParameterType?.DynamicProperty, GlobalContext.FunctionList); - - /// - /// Return type when determined at runtime/by dynamic intellisense. - /// - public ConnectorType ReturnParameterType - { - get - { - EnsureInitialized(); - return _returnType; - } - } - - /// - /// Parameter types used for TexlFunction. - /// - internal DType[] ParameterTypes => _parameterTypes ?? GetParamTypes(); - - private DType[] _parameterTypes; - - /// - /// Contains the list of functions in the same swagger file, used for resolving dynamic schema/property. - /// Also contains all global values. - /// - internal ConnectorGlobalContext GlobalContext { get; } - - // Those parameters are protected by EnsureInitialized - private int _arityMin; - - private int _arityMax; - - private ConnectorParameter[] _requiredParameters; - - private ConnectorParameter[] _hiddenRequiredParameters; - - private ConnectorParameter[] _optionalParameters; - - private ConnectorType _returnType; - - private bool _isSupported; - - private string _notSupportedReason; - - private List _warnings; - - // Those properties are only used by HttpFunctionInvoker - internal ConnectorParameterInternals _internals = null; - - private readonly ConnectorLogger _configurationLogger = null; - + +namespace Microsoft.PowerFx.Connectors +{ + /// + /// Represents a connector function. + /// + [DebuggerDisplay("{Name}")] + public class ConnectorFunction + { + /// + /// Normalized name of the function. + /// + public string Name { get; } + + /// + /// Namespace of the function (not contained in swagger file). + /// + public string Namespace => ConnectorSettings.Namespace; + + /// + /// ConnectorSettings. Contains the Namespace and other parameters. + /// + public ConnectorSettings ConnectorSettings { get; } + + /// + /// Defines if the function is supported or contains unsupported elements. + /// + public bool IsSupported + { + get + { + EnsureInitialized(); + return _isSupported; + } + } + + /// + /// Reason for which the function isn't supported. + /// + public string NotSupportedReason + { + get + { + EnsureInitialized(); + return _notSupportedReason; + } + } + + /// + /// Warnings to be reported to end user. + /// + public IReadOnlyCollection Warnings + { + get + { + EnsureInitialized(); + return _warnings; + } + } + + /// + /// Defines if the function is deprecated. + /// + public bool IsDeprecated => Operation.Deprecated; + + /// + /// Page Link as defined in the x-ms-pageable extension. + /// This is the name of the property that will host the URL, not the URL itself. + /// + public string PageLink => Operation.PageLink(); + + /// + /// Name as it appears in the swagger file. + /// + public string OriginalName => Operation.OperationId; + + /// + /// Operation's description. + /// + public string Description => Operation.Description ?? $"Invoke {Name}"; + + /// + /// Operation's summary. + /// + public string Summary => Operation.Summary; + + /// + /// Operation's path. Includes the base path if any. + /// + public string OperationPath { get; } + + /// + /// HTTP method. + /// + public HttpMethod HttpMethod { get; } + + /// + /// Defines behavioral functions. + /// HTTP/1.1 spec states that only GET and HEAD requests are 'safe' by default. + /// https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html. + /// + public bool IsBehavior => HttpMethod != HttpMethod.Get && HttpMethod != HttpMethod.Head; + + /// + /// Defines if the function is pageable (using x-ms-pageable extension). + /// + public bool IsPageable => !string.IsNullOrEmpty(PageLink); + + /// + /// Visibility defined as "x-ms-visibility" string content. + /// + public string Visibility => Operation.GetVisibility(); + + /// + /// When "x-ms-visibility" is set to "internal". + /// https://learn.microsoft.com/en-us/connectors/custom-connectors/openapi-extensions#x-ms-visibility. + /// + public bool IsInternal => Operation.IsInternal(); + + /// + /// Defined as "x-ms-require-user-confirmation" boolean content. + /// + public bool RequiresUserConfirmation => Operation.GetRequiresUserConfirmation(); + + /// + /// Return type of the function. + /// + public FormulaType ReturnType => Operation.GetReturnType(ConnectorSettings); + + /// + /// Minimum number of arguments. + /// + public int ArityMin + { + get + { + EnsureInitialized(); + return _arityMin; + } + } + + /// + /// Maximum number of arguments. + /// + public int ArityMax + { + get + { + EnsureInitialized(); + return _arityMax; + } + } + + /// + /// Required parameters. + /// + public ConnectorParameter[] RequiredParameters + { + get + { + EnsureInitialized(); + return _requiredParameters; + } + } + + /// + /// Optional parameters. + /// + public ConnectorParameter[] OptionalParameters + { + get + { + EnsureInitialized(); + return _optionalParameters; + } + } + + /// + /// Swagger's operation. + /// + internal OpenApiOperation Operation { get; } + + /// + /// Hidden required parameters. + /// Defined as + /// - part "required" swagger array + /// - "x-ms-visibility" string set to "internal" + /// - has a default value. + /// + internal ConnectorParameter[] HiddenRequiredParameters + { + get + { + EnsureInitialized(); + return _hiddenRequiredParameters; + } + } + + /// + /// OpenApiServers defined in the document. + /// + internal IEnumerable Servers { get; init; } + + /// + /// Filtered function. + /// + internal bool Filtered { get; } + + /// + /// Dynamic schema extension on return type (response). + /// + internal ConnectorDynamicSchema DynamicReturnSchema => EnsureConnectorFunction(ReturnParameterType?.DynamicSchema, GlobalContext.FunctionList); + + /// + /// Dynamic schema extension on return type (response). + /// + internal ConnectorDynamicProperty DynamicReturnProperty => EnsureConnectorFunction(ReturnParameterType?.DynamicProperty, GlobalContext.FunctionList); + + /// + /// Return type when determined at runtime/by dynamic intellisense. + /// + public ConnectorType ReturnParameterType + { + get + { + EnsureInitialized(); + return _returnType; + } + } + + /// + /// Parameter types used for TexlFunction. + /// + internal DType[] ParameterTypes => _parameterTypes ?? GetParamTypes(); + + private DType[] _parameterTypes; + + /// + /// Indicates if the specific handling of the body takes place in this functions. + /// This is only when UseItemDynamicPropertiesSpecialHandling is enabled in ConnectorSettings. + /// + public bool SpecialBodyHandling => _specialBodyHandling; + + private bool _specialBodyHandling = false; + + /// + /// Contains the list of functions in the same swagger file, used for resolving dynamic schema/property. + /// Also contains all global values. + /// + internal ConnectorGlobalContext GlobalContext { get; } + + // Those parameters are protected by EnsureInitialized + private int _arityMin; + + private int _arityMax; + + private ConnectorParameter[] _requiredParameters; + + private ConnectorParameter[] _hiddenRequiredParameters; + + private ConnectorParameter[] _optionalParameters; + + private ConnectorType _returnType; + + private bool _isSupported; + + private string _notSupportedReason; + + private List _warnings; + + // Those properties are only used by HttpFunctionInvoker + internal ConnectorParameterInternals _internals = null; + + private readonly ConnectorLogger _configurationLogger = null; + internal ConnectorFunction(OpenApiOperation openApiOperation, bool isSupported, string notSupportedReason, string name, string operationPath, HttpMethod httpMethod, ConnectorSettings connectorSettings, List functionList, - ConnectorLogger configurationLogger, IReadOnlyDictionary globalValues) - { - Operation = openApiOperation; - Name = name; - OperationPath = operationPath; - HttpMethod = httpMethod; + ConnectorLogger configurationLogger, IReadOnlyDictionary globalValues) + { + Operation = openApiOperation; + Name = name; + OperationPath = operationPath; + HttpMethod = httpMethod; ConnectorSettings = connectorSettings; - GlobalContext = new ConnectorGlobalContext(functionList ?? throw new ArgumentNullException(nameof(functionList)), globalValues); - - _configurationLogger = configurationLogger; - _isSupported = isSupported; - _notSupportedReason = notSupportedReason ?? (isSupported ? string.Empty : throw new PowerFxConnectorException("Internal error on not supported reason")); - - int nvCount = globalValues?.Count(nv => nv.Key != "connectionId") ?? 0; - if (nvCount > 0) - { - EnsureInitialized(); - Filtered = _requiredParameters.Length < nvCount || !_requiredParameters.Take(nvCount).All(rp => globalValues.Keys.Contains(rp.Name)); - - if (!Filtered) - { - _requiredParameters = _requiredParameters.Skip(nvCount).ToArray(); - _arityMin -= nvCount; - _arityMax -= nvCount; - } - } - } - - internal void SetUnsupported(string notSupportedReason) - { - _isSupported = false; - _notSupportedReason = string.IsNullOrEmpty(_notSupportedReason) ? notSupportedReason : $"{_notSupportedReason}, {notSupportedReason}"; - } - - /// - /// Get connector function parameter suggestions. - /// - /// Known parameters. - /// Parameter for which we need a set of suggestions. - /// Runtime connector context. - /// Cancellation token. - /// ConnectorParameters class with suggestions. - public async Task GetParameterSuggestionsAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); - ConnectorParameters parameters = await GetParameterSuggestionsInternalAsync(knownParameters, connectorParameter, runtimeContext, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, returning {LogConnectorParameters(parameters)}"); - return parameters; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); - throw; - } - } - - internal async Task GetParameterSuggestionsInternalAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); - - List parametersWithSuggestions = new List(); - ConnectorEnhancedSuggestions suggestions = GetConnectorSuggestionsInternalAsync(knownParameters, connectorParameter.ConnectorType, runtimeContext, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); - - runtimeContext.ExecutionLogger?.LogDebug($"In {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsInternalAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); - foreach (ConnectorParameter parameter in RequiredParameters.Union(OptionalParameters)) - { - NamedValue namedValue = knownParameters.FirstOrDefault(p => p.Name == parameter.Name); - ConnectorParameterWithSuggestions cpws = new ConnectorParameterWithSuggestions(parameter, namedValue?.Value, connectorParameter.Name, suggestions, knownParameters); - parametersWithSuggestions.Add(cpws); - } - - ConnectorParameters connectorParameters = new ConnectorParameters() - { - IsCompleted = suggestions != null && parametersWithSuggestions.All(p => !p.Suggestions.Any()), - ParametersWithSuggestions = parametersWithSuggestions.ToArray() - }; - - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, with {LogConnectorParameters(connectorParameters)}"); - return connectorParameters; - } - - /// - /// Get connector function parameter suggestions. - /// - /// Known parameters. - /// Connector type for which we need a set of suggestions. - /// Runtime connector context. - /// Cancellation token. - /// ConnectorParameters class with suggestions. - public async Task GetConnectorSuggestionsAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorType(connectorType)}"); - ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsInternalAsync(knownParameters, connectorType, runtimeContext, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, returning from {nameof(GetConnectorSuggestionsInternalAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); - return suggestions; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorType(connectorType)}, {LogException(ex)}"); - throw; - } - } - - internal async Task GetConnectorSuggestionsInternalAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorType(connectorType)}"); - - if (connectorType != null) - { - if (connectorType.DynamicList != null) - { - ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsFromDynamicListAsync(knownParameters, runtimeContext, connectorType.DynamicList, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicListAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); - return suggestions; - } - - // BuiltInOperation and capability are currently not supported - if (connectorType.DynamicValues != null) - { - ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsFromDynamicValueAsync(knownParameters, runtimeContext, connectorType.DynamicValues, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicValueAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); - return suggestions; - } - - ConnectorType outputConnectorType = null; - SuggestionMethod suggestionMethod = SuggestionMethod.None; - - if (connectorType.DynamicProperty != null && !string.IsNullOrEmpty(connectorType.DynamicProperty.ItemValuePath)) - { - outputConnectorType = await GetConnectorSuggestionsFromDynamicPropertyAsync(knownParameters, runtimeContext, connectorType.DynamicProperty, cancellationToken).ConfigureAwait(false); - suggestionMethod = SuggestionMethod.DynamicProperty; - } - else if (connectorType.DynamicSchema != null && !string.IsNullOrEmpty(connectorType.DynamicSchema.ValuePath)) - { - outputConnectorType = await GetConnectorSuggestionsFromDynamicSchemaAsync(knownParameters, runtimeContext, connectorType.DynamicSchema, cancellationToken).ConfigureAwait(false); - suggestionMethod = SuggestionMethod.DynamicSchema; - } - - if (outputConnectorType != null && outputConnectorType.FormulaType is RecordType rt) - { - ConnectorEnhancedSuggestions suggestions = new ConnectorEnhancedSuggestions(suggestionMethod, rt.FieldNames.Select(fn => new ConnectorSuggestion(FormulaValue.NewBlank(rt.GetFieldType(fn)), fn)).ToList(), outputConnectorType); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {(suggestionMethod == SuggestionMethod.DynamicProperty ? nameof(GetConnectorSuggestionsFromDynamicPropertyAsync) : nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))} with {LogConnectorEnhancedSuggestions(suggestions)}"); - return suggestions; - } - - if (connectorType.DynamicList == null & connectorType.DynamicValues == null && connectorType.DynamicProperty == null && connectorType.DynamicSchema == null) - { - runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null as no dynamic extension for {LogConnectorType(connectorType)}"); - } - else - { - runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null for {LogConnectorType(connectorType)}"); - } - - return null; - } - - runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null as connectorType is null"); - return null; - } - - /// - /// Dynamic intellisense (dynamic property/schema) on a given parameter. - /// - /// Known parameters. - /// Parameter for which we need the (dynamic) type. - /// Connector context. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); - - ConnectorType result = await GetConnectorParameterTypeAsync(knownParameters, connectorParameter, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(result)}"); - return result; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); - throw; - } - } - - /// - /// Dynamic intellisense (dynamic property/schema) on a given parameter. - /// - /// Known parameters. - /// Parameter for which we need the (dynamic) type. - /// Connector context. - /// Max number of recursive network calls. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) - { - return await GetConnectorParameterTypeAsync(knownParameters, connectorParameter, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); - } - - internal async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, with {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft}, {LogConnectorParameter(connectorParameter)}"); - - ConnectorType result = await GetConnectorTypeInternalAsync(knownParameters, connectorParameter.ConnectorType ?? ReturnParameterType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(result)}, callsLeft {maxCalls.CallsLeft}"); - return result; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); - throw; - } - } - - /// - /// Dynamic intellisense (dynamic property/schema) on a given connector type (coming from a parameter or returned from GetConnectorTypeAsync). - /// - /// Known parameters. - /// Connector type for which we need the (dynamic) type. - /// Connector context. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorTypeAsync))}, with {LogKnownParameters(knownParameters)} for {LogConnectorType(connectorType)}"); - ConnectorType connectorType2 = await GetConnectorTypeAsync(knownParameters, connectorType, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(connectorType2)}"); - return connectorType2; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorType(connectorType)}, {LogException(ex)}"); - throw; - } - } - - /// - /// Dynamic intellisense (dynamic property/schema) on a given connector type (coming from a parameter or returned from GetConnectorTypeAsync). - /// - /// Known parameters. - /// Connector type for which we need the (dynamic) type. - /// Connector context. - /// Max number of recursive network calls. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) - { - return await GetConnectorTypeAsync(knownParameters, connectorType, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); - } - - internal async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorTypeAsync))}, with {LogKnownParameters(knownParameters)} and callsLeft {maxCalls.CallsLeft} for {LogConnectorType(connectorType)}"); - ConnectorType connectorType2 = await GetConnectorTypeInternalAsync(knownParameters, connectorType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(connectorType2)}, callsLeft {maxCalls.CallsLeft}"); - return connectorType2; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorTypeAsync))}, Context {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft} {LogConnectorType(connectorType)}, {LogException(ex)}"); - throw; - } - } - - internal async Task GetConnectorTypeInternalAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, with {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft} for {LogConnectorType(connectorType)}"); - - if (connectorType.DynamicProperty != null && !string.IsNullOrEmpty(connectorType.DynamicProperty.ItemValuePath)) - { - maxCalls.CallsLeft--; - ConnectorType result = await GetConnectorSuggestionsFromDynamicPropertyAsync(knownParameters, runtimeContext, connectorType.DynamicProperty, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicPropertyAsync)} with {LogConnectorType(result)}"); - return result; - } - else if (connectorType.DynamicSchema != null && !string.IsNullOrEmpty(connectorType.DynamicSchema.ValuePath)) - { - maxCalls.CallsLeft--; - ConnectorType result = await GetConnectorSuggestionsFromDynamicSchemaAsync(knownParameters, runtimeContext, connectorType.DynamicSchema, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicSchemaAsync)} with {LogConnectorType(result)}"); - return result; - } - else if (connectorType.DynamicList != null && !string.IsNullOrEmpty(connectorType.DynamicList.ItemPath)) - { - maxCalls.CallsLeft--; - ConnectorEnhancedSuggestions result = await GetConnectorSuggestionsFromDynamicListAsync(knownParameters, runtimeContext, connectorType.DynamicList, cancellationToken).ConfigureAwait(false); - - // This is an OptionSet - if (result != null && (connectorType.FormulaType is DecimalType || connectorType.FormulaType is NumberType)) - { - ConnectorType connectorType2 = GetOptionSetFromSuggestions(connectorType, result); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicListAsync)} with {LogConnectorType(connectorType2)}"); - return connectorType2; - } - } - else if (connectorType.DynamicValues != null && !string.IsNullOrEmpty(connectorType.DynamicValues.ValuePath)) - { - maxCalls.CallsLeft--; - ConnectorEnhancedSuggestions result = await GetConnectorSuggestionsFromDynamicValueAsync(knownParameters, runtimeContext, connectorType.DynamicValues, cancellationToken).ConfigureAwait(false); - - // This is an OptionSet - if (result != null && (connectorType.FormulaType is DecimalType || connectorType.FormulaType is NumberType)) - { - ConnectorType connectorType2 = GetOptionSetFromSuggestions(connectorType, result); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicValueAsync)} with {LogConnectorType(connectorType2)}"); - return connectorType2; - } - } - else if (connectorType.ContainsDynamicIntellisense && connectorType.Fields.Any() && connectorType.FormulaType is AggregateType aggregateType) - { - List fieldTypes = new List(); - RecordType recordType = RecordType.Empty(); - - foreach (ConnectorType field in connectorType.Fields) - { - ConnectorType newFieldType = field; - - while (newFieldType.ContainsDynamicIntellisense && maxCalls.CallsLeft > 0) - { - ConnectorType newFieldType2 = await GetConnectorTypeInternalAsync(knownParameters, newFieldType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); - - if (newFieldType2 == null) - { - break; - } - - newFieldType = newFieldType2; - } - - if (maxCalls.CallsLeft <= 0) - { - runtimeContext.ExecutionLogger?.LogDebug($"In {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, callsLeft {maxCalls.CallsLeft}."); - } - - // field's name correspond to the key of the fhe field in the record, we need to set it to wire it up correctly to the updated record type - newFieldType.Name = field.Name; - fieldTypes.Add(newFieldType); - recordType = recordType.Add(field.Name, newFieldType.FormulaType, field.DisplayName); - } - - FormulaType formulaType = connectorType.FormulaType is RecordType ? recordType : recordType.ToTable(); - ConnectorType newConnectorType = new ConnectorType(connectorType, fieldTypes.ToArray(), formulaType); - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicSchemaAsync)} with {LogConnectorType(newConnectorType)}"); - return newConnectorType; - } - - runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning null as no dynamic extension defined for {LogConnectorType(connectorType)}"); - return null; - } - - private static ConnectorType GetOptionSetFromSuggestions(ConnectorType connectorType, ConnectorEnhancedSuggestions result) - { - IEnumerable> values = result.ConnectorSuggestions.Suggestions.Select(cs => new KeyValuePair(cs.DisplayName, cs.Suggestion.AsDouble())); - EnumSymbol optionSet = new EnumSymbol(new DName(connectorType.Name), DType.Number, values); - OpenApiParameter openApiParameter = new OpenApiParameter() - { - Name = connectorType.Name, - Required = connectorType.IsRequired, - Extensions = new Dictionary() - { - { Constants.XMsVisibility, new OpenApiString(connectorType.Visibility.ToString()) }, - { Constants.XMsMediaKind, new OpenApiString(connectorType.MediaKind.ToString()) } - } - }; - - ISwaggerSchema schema = new SwaggerSchema(connectorType.Schema) - { - Enum = optionSet.EnumType.ValueTree.GetPairs().Select(kvp => new OpenApiDouble((double)kvp.Value.Object) as IOpenApiAny).ToList() - }; - - OpenApiArray array = new OpenApiArray(); - array.AddRange(optionSet.EnumType.ValueTree.GetPairs().Select(kvp => new OpenApiString(kvp.Key))); - - schema.Extensions.Add(new KeyValuePair(Constants.XMsEnumDisplayName, array)); - - // For now, we keep the original formula type (number/string/bool...) - return new ConnectorType(schema, SwaggerParameter.New(openApiParameter), connectorType.FormulaType /* optionSet.FormulaType */); - } - - /// - /// Dynamic intellisense on return value. - /// - /// Known parameters. - /// Connector context. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, with {LogKnownParameters(knownParameters)}"); - ConnectorType connectorType = await GetConnectorReturnTypeAsync(knownParameters, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, returning {nameof(GetConnectorTypeAsync)}, with {LogConnectorType(connectorType)}"); - return connectorType; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, Context {LogKnownParameters(knownParameters)}, {LogException(ex)}"); - throw; - } - } - - /// - /// Dynamic intellisense on return value. - /// - /// Known parameters. - /// Connector context. - /// Max number of recursive network calls. - /// Cancellation token. - /// Formula Type determined by dynamic Intellisense. - public async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) - { - return await GetConnectorReturnTypeAsync(knownParameters, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); - } - - internal async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, with {LogKnownParameters(knownParameters)} and callsLeft {maxCalls.CallsLeft}"); - ConnectorType connectorType = await GetConnectorTypeAsync(knownParameters, ReturnParameterType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, returning {nameof(GetConnectorTypeAsync)}, with {LogConnectorType(connectorType)}"); - return connectorType; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, Context {LogKnownParameters(knownParameters)}, {LogException(ex)}"); - throw; - } - } - - /// - /// Generates a Power Fx expression that will invoke this function with the given parameters. - /// - /// Parameters. - /// Power Fx expression. - public string GetExpression(ConnectorParameters parameters) - { - if (!parameters.IsCompleted) - { - return null; - } - - StringBuilder sb = new StringBuilder($@"{Namespace}.{Name}({string.Join(", ", parameters.ParametersWithSuggestions.Take(Math.Min(parameters.ParametersWithSuggestions.Length, ArityMax - 1)).Select(p => p.Value.ToExpression()))}"); - - if (parameters.ParametersWithSuggestions.Length > ArityMax - 1) - { - ConnectorParameterWithSuggestions lastParam = parameters.ParametersWithSuggestions.Last(); - sb.Append($@", {{ "); - - List<(string name, FormulaValue fv)> nameValueAssociations = lastParam.ParameterNames.Zip(lastParam.Values, (name, fv) => (name, fv)).ToList(); - int i = 0; - - foreach ((string name, FormulaValue fv) in nameValueAssociations) - { - sb.Append(name); - sb.Append(": "); - fv.ToExpression(sb, new FormulaValueSerializerSettings()); - - if (++i != nameValueAssociations.Count) - { - sb.Append(", "); - } - } - - sb.Append(" }"); - } - - sb.Append(")"); - - return sb.ToString(); - } - - /// - /// Call connector function. - /// - /// Arguments. - /// RuntimeConnectorContext. - /// Cancellation token. - /// Function result. - public Task InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - return InvokeAsync(arguments, runtimeContext, null, cancellationToken); - } - - /// - /// Call connector function. - /// - /// Arguments. - /// RuntimeConnectorContext. - /// The output type that should be used during output parsing. - /// Cancellation token. - /// Function result. - public async Task InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(InvokeAsync))}, with {LogArguments(arguments)}"); - FormulaValue formulaValue = await InvokeInternalAsync(arguments, runtimeContext, outputTypeOverride, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(InvokeAsync))}, returning from {nameof(InvokeInternalAsync)}, with {LogFormulaValue(formulaValue)}"); - return formulaValue; - } - catch (Exception ex) - { - runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(InvokeAsync))}, Context {LogArguments(arguments)}, {LogException(ex)}"); - throw; - } - } - - internal Task InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - return InvokeInternalAsync(arguments, runtimeContext, null, cancellationToken); - } - - internal async Task InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - EnsureInitialized(); - runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(InvokeInternalAsync))}, with {LogArguments(arguments)}"); - - if (!IsSupported) - { - throw new InvalidOperationException($"In namespace {Namespace}, function {Name} is not supported."); - } - - FormulaValue ev = arguments.Where(arg => arg is ErrorValue).FirstOrDefault(); - if (ev != null) - { - return ev; - } - - BaseRuntimeConnectorContext context = ReturnParameterType.Binary ? runtimeContext.WithRawResults() : runtimeContext; - ScopedHttpFunctionInvoker invoker = new ScopedHttpFunctionInvoker(DPath.Root.Append(DName.MakeValid(Namespace, out _)), Name, Namespace, new HttpFunctionInvoker(this, context), context.ThrowOnError); - FormulaValue result = await invoker.InvokeAsync(arguments, context, outputTypeOverride, cancellationToken).ConfigureAwait(false); - FormulaValue formulaValue = await PostProcessResultAsync(result, runtimeContext, invoker, cancellationToken).ConfigureAwait(false); - - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(InvokeInternalAsync))}, returning {LogFormulaValue(formulaValue)}"); - return formulaValue; - } - - private async Task PostProcessResultAsync(FormulaValue result, BaseRuntimeConnectorContext runtimeContext, ScopedHttpFunctionInvoker invoker, CancellationToken cancellationToken) - { - ExpressionError er = null; - - if (result is ErrorValue ev && (er = ev.Errors.FirstOrDefault(e => e.Kind == ErrorKind.Network || e.Kind == ErrorKind.InvalidJSON || e.Kind == ErrorKind.InvalidArgument)) != null) - { - runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(PostProcessResultAsync))}, ErrorValue is returned with {er.Message}"); - ExpressionError newError = er is HttpExpressionError her - ? new HttpExpressionError(her.StatusCode) { Kind = er.Kind, Severity = er.Severity, Message = $"{DPath.Root.Append(new DName(Namespace)).ToDottedSyntax()}.{Name} failed: {er.Message}" } - : new ExpressionError() { Kind = er.Kind, Severity = er.Severity, Message = $"{DPath.Root.Append(new DName(Namespace)).ToDottedSyntax()}.{Name} failed: {er.Message}" }; + GlobalContext = new ConnectorGlobalContext(functionList ?? throw new ArgumentNullException(nameof(functionList)), globalValues); + + _configurationLogger = configurationLogger; + _isSupported = isSupported; + _notSupportedReason = notSupportedReason ?? (isSupported ? string.Empty : throw new PowerFxConnectorException("Internal error on not supported reason")); + + int nvCount = globalValues?.Count(nv => nv.Key != "connectionId") ?? 0; + if (nvCount > 0) + { + EnsureInitialized(); + Filtered = _requiredParameters.Length < nvCount || !_requiredParameters.Take(nvCount).All(rp => globalValues.Keys.Contains(rp.Name)); + + if (!Filtered) + { + _requiredParameters = _requiredParameters.Skip(nvCount).ToArray(); + _arityMin -= nvCount; + _arityMax -= nvCount; + } + } + } + + internal void SetUnsupported(string notSupportedReason) + { + _isSupported = false; + _notSupportedReason = string.IsNullOrEmpty(_notSupportedReason) ? notSupportedReason : $"{_notSupportedReason}, {notSupportedReason}"; + } + + /// + /// Get connector function parameter suggestions. + /// + /// Known parameters. + /// Parameter for which we need a set of suggestions. + /// Runtime connector context. + /// Cancellation token. + /// ConnectorParameters class with suggestions. + public async Task GetParameterSuggestionsAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); + ConnectorParameters parameters = await GetParameterSuggestionsInternalAsync(knownParameters, connectorParameter, runtimeContext, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, returning {LogConnectorParameters(parameters)}"); + return parameters; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetParameterSuggestionsAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); + throw; + } + } + + internal async Task GetParameterSuggestionsInternalAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); + + List parametersWithSuggestions = new List(); + ConnectorEnhancedSuggestions suggestions = GetConnectorSuggestionsInternalAsync(knownParameters, connectorParameter.ConnectorType, runtimeContext, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + + runtimeContext.ExecutionLogger?.LogDebug($"In {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsInternalAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); + foreach (ConnectorParameter parameter in RequiredParameters.Union(OptionalParameters)) + { + NamedValue namedValue = knownParameters.FirstOrDefault(p => p.Name == parameter.Name); + ConnectorParameterWithSuggestions cpws = new ConnectorParameterWithSuggestions(parameter, namedValue?.Value, connectorParameter.Name, suggestions, knownParameters); + parametersWithSuggestions.Add(cpws); + } + + ConnectorParameters connectorParameters = new ConnectorParameters() + { + IsCompleted = suggestions != null && parametersWithSuggestions.All(p => !p.Suggestions.Any()), + ParametersWithSuggestions = parametersWithSuggestions.ToArray() + }; + + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetParameterSuggestionsInternalAsync))}, with {LogConnectorParameters(connectorParameters)}"); + return connectorParameters; + } + + /// + /// Get connector function parameter suggestions. + /// + /// Known parameters. + /// Connector type for which we need a set of suggestions. + /// Runtime connector context. + /// Cancellation token. + /// ConnectorParameters class with suggestions. + public async Task GetConnectorSuggestionsAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorType(connectorType)}"); + ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsInternalAsync(knownParameters, connectorType, runtimeContext, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, returning from {nameof(GetConnectorSuggestionsInternalAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); + return suggestions; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorSuggestionsAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorType(connectorType)}, {LogException(ex)}"); + throw; + } + } + + internal async Task GetConnectorSuggestionsInternalAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorType(connectorType)}"); + + if (connectorType != null) + { + if (connectorType.DynamicList != null) + { + ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsFromDynamicListAsync(knownParameters, runtimeContext, connectorType.DynamicList, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicListAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); + return suggestions; + } + + // BuiltInOperation and capability are currently not supported + if (connectorType.DynamicValues != null) + { + ConnectorEnhancedSuggestions suggestions = await GetConnectorSuggestionsFromDynamicValueAsync(knownParameters, runtimeContext, connectorType.DynamicValues, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicValueAsync)} with {LogConnectorEnhancedSuggestions(suggestions)}"); + return suggestions; + } + + ConnectorType outputConnectorType = null; + SuggestionMethod suggestionMethod = SuggestionMethod.None; + + if (connectorType.DynamicProperty != null && !string.IsNullOrEmpty(connectorType.DynamicProperty.ItemValuePath)) + { + outputConnectorType = await GetConnectorSuggestionsFromDynamicPropertyAsync(knownParameters, runtimeContext, connectorType.DynamicProperty, cancellationToken).ConfigureAwait(false); + suggestionMethod = SuggestionMethod.DynamicProperty; + } + else if (connectorType.DynamicSchema != null && !string.IsNullOrEmpty(connectorType.DynamicSchema.ValuePath)) + { + outputConnectorType = await GetConnectorSuggestionsFromDynamicSchemaAsync(knownParameters, runtimeContext, connectorType.DynamicSchema, cancellationToken).ConfigureAwait(false); + suggestionMethod = SuggestionMethod.DynamicSchema; + } + + if (outputConnectorType != null && outputConnectorType.FormulaType is RecordType rt) + { + ConnectorEnhancedSuggestions suggestions = new ConnectorEnhancedSuggestions(suggestionMethod, rt.FieldNames.Select(fn => new ConnectorSuggestion(FormulaValue.NewBlank(rt.GetFieldType(fn)), fn)).ToList(), outputConnectorType); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning from {(suggestionMethod == SuggestionMethod.DynamicProperty ? nameof(GetConnectorSuggestionsFromDynamicPropertyAsync) : nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))} with {LogConnectorEnhancedSuggestions(suggestions)}"); + return suggestions; + } + + if (connectorType.DynamicList == null & connectorType.DynamicValues == null && connectorType.DynamicProperty == null && connectorType.DynamicSchema == null) + { + runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null as no dynamic extension for {LogConnectorType(connectorType)}"); + } + else + { + runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null for {LogConnectorType(connectorType)}"); + } + + return null; + } + + runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsInternalAsync))}, returning null as connectorType is null"); + return null; + } + + /// + /// Dynamic intellisense (dynamic property/schema) on a given parameter. + /// + /// Known parameters. + /// Parameter for which we need the (dynamic) type. + /// Connector context. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, with {LogKnownParameters(knownParameters)}, {LogConnectorParameter(connectorParameter)}"); + + ConnectorType result = await GetConnectorParameterTypeAsync(knownParameters, connectorParameter, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(result)}"); + return result; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); + throw; + } + } + + /// + /// Dynamic intellisense (dynamic property/schema) on a given parameter. + /// + /// Known parameters. + /// Parameter for which we need the (dynamic) type. + /// Connector context. + /// Max number of recursive network calls. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) + { + return await GetConnectorParameterTypeAsync(knownParameters, connectorParameter, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); + } + + internal async Task GetConnectorParameterTypeAsync(NamedValue[] knownParameters, ConnectorParameter connectorParameter, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, with {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft}, {LogConnectorParameter(connectorParameter)}"); + + ConnectorType result = await GetConnectorTypeInternalAsync(knownParameters, connectorParameter.ConnectorType ?? ReturnParameterType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(result)}, callsLeft {maxCalls.CallsLeft}"); + return result; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorParameterTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorParameter(connectorParameter)}, {LogException(ex)}"); + throw; + } + } + + /// + /// Dynamic intellisense (dynamic property/schema) on a given connector type (coming from a parameter or returned from GetConnectorTypeAsync). + /// + /// Known parameters. + /// Connector type for which we need the (dynamic) type. + /// Connector context. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorTypeAsync))}, with {LogKnownParameters(knownParameters)} for {LogConnectorType(connectorType)}"); + ConnectorType connectorType2 = await GetConnectorTypeAsync(knownParameters, connectorType, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(connectorType2)}"); + return connectorType2; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorTypeAsync))}, Context {LogKnownParameters(knownParameters)} {LogConnectorType(connectorType)}, {LogException(ex)}"); + throw; + } + } + + /// + /// Dynamic intellisense (dynamic property/schema) on a given connector type (coming from a parameter or returned from GetConnectorTypeAsync). + /// + /// Known parameters. + /// Connector type for which we need the (dynamic) type. + /// Connector context. + /// Max number of recursive network calls. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) + { + return await GetConnectorTypeAsync(knownParameters, connectorType, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); + } + + internal async Task GetConnectorTypeAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorTypeAsync))}, with {LogKnownParameters(knownParameters)} and callsLeft {maxCalls.CallsLeft} for {LogConnectorType(connectorType)}"); + ConnectorType connectorType2 = await GetConnectorTypeInternalAsync(knownParameters, connectorType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorTypeAsync))}, returning from {nameof(GetConnectorTypeInternalAsync)} with {LogConnectorType(connectorType2)}, callsLeft {maxCalls.CallsLeft}"); + return connectorType2; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorTypeAsync))}, Context {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft} {LogConnectorType(connectorType)}, {LogException(ex)}"); + throw; + } + } + + internal async Task GetConnectorTypeInternalAsync(NamedValue[] knownParameters, ConnectorType connectorType, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, with {LogKnownParameters(knownParameters)}, callsLeft {maxCalls.CallsLeft} for {LogConnectorType(connectorType)}"); + + if (connectorType.DynamicProperty != null && !string.IsNullOrEmpty(connectorType.DynamicProperty.ItemValuePath)) + { + maxCalls.CallsLeft--; + ConnectorType result = await GetConnectorSuggestionsFromDynamicPropertyAsync(knownParameters, runtimeContext, connectorType.DynamicProperty, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicPropertyAsync)} with {LogConnectorType(result)}"); + return result; + } + else if (connectorType.DynamicSchema != null && !string.IsNullOrEmpty(connectorType.DynamicSchema.ValuePath)) + { + maxCalls.CallsLeft--; + ConnectorType result = await GetConnectorSuggestionsFromDynamicSchemaAsync(knownParameters, runtimeContext, connectorType.DynamicSchema, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicSchemaAsync)} with {LogConnectorType(result)}"); + return result; + } + else if (connectorType.DynamicList != null && !string.IsNullOrEmpty(connectorType.DynamicList.ItemPath)) + { + maxCalls.CallsLeft--; + ConnectorEnhancedSuggestions result = await GetConnectorSuggestionsFromDynamicListAsync(knownParameters, runtimeContext, connectorType.DynamicList, cancellationToken).ConfigureAwait(false); + + // This is an OptionSet + if (result != null && (connectorType.FormulaType is DecimalType || connectorType.FormulaType is NumberType)) + { + ConnectorType connectorType2 = GetOptionSetFromSuggestions(connectorType, result); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicListAsync)} with {LogConnectorType(connectorType2)}"); + return connectorType2; + } + } + else if (connectorType.DynamicValues != null && !string.IsNullOrEmpty(connectorType.DynamicValues.ValuePath)) + { + maxCalls.CallsLeft--; + ConnectorEnhancedSuggestions result = await GetConnectorSuggestionsFromDynamicValueAsync(knownParameters, runtimeContext, connectorType.DynamicValues, cancellationToken).ConfigureAwait(false); + + // This is an OptionSet + if (result != null && (connectorType.FormulaType is DecimalType || connectorType.FormulaType is NumberType)) + { + ConnectorType connectorType2 = GetOptionSetFromSuggestions(connectorType, result); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicValueAsync)} with {LogConnectorType(connectorType2)}"); + return connectorType2; + } + } + else if (connectorType.ContainsDynamicIntellisense && connectorType.Fields.Any() && connectorType.FormulaType is AggregateType aggregateType) + { + List fieldTypes = new List(); + RecordType recordType = RecordType.Empty(); + + foreach (ConnectorType field in connectorType.Fields) + { + ConnectorType newFieldType = field; + + while (newFieldType.ContainsDynamicIntellisense && maxCalls.CallsLeft > 0) + { + ConnectorType newFieldType2 = await GetConnectorTypeInternalAsync(knownParameters, newFieldType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); + + if (newFieldType2 == null) + { + break; + } + + newFieldType = newFieldType2; + } + + if (maxCalls.CallsLeft <= 0) + { + runtimeContext.ExecutionLogger?.LogDebug($"In {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, callsLeft {maxCalls.CallsLeft}."); + } + + // field's name correspond to the key of the fhe field in the record, we need to set it to wire it up correctly to the updated record type + newFieldType.Name = field.Name; + fieldTypes.Add(newFieldType); + recordType = recordType.Add(field.Name, newFieldType.FormulaType, field.DisplayName); + } + + FormulaType formulaType = connectorType.FormulaType is RecordType ? recordType : recordType.ToTable(); + ConnectorType newConnectorType = new ConnectorType(connectorType, fieldTypes.ToArray(), formulaType); + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning from {nameof(GetConnectorSuggestionsFromDynamicSchemaAsync)} with {LogConnectorType(newConnectorType)}"); + return newConnectorType; + } + + runtimeContext.ExecutionLogger?.LogWarning($"Exiting {this.LogFunction(nameof(GetConnectorTypeInternalAsync))}, returning null as no dynamic extension defined for {LogConnectorType(connectorType)}"); + return null; + } + + private static ConnectorType GetOptionSetFromSuggestions(ConnectorType connectorType, ConnectorEnhancedSuggestions result) + { + IEnumerable> values = result.ConnectorSuggestions.Suggestions.Select(cs => new KeyValuePair(cs.DisplayName, cs.Suggestion.AsDouble())); + EnumSymbol optionSet = new EnumSymbol(new DName(connectorType.Name), DType.Number, values); + OpenApiParameter openApiParameter = new OpenApiParameter() + { + Name = connectorType.Name, + Required = connectorType.IsRequired, + Extensions = new Dictionary() + { + { Constants.XMsVisibility, new OpenApiString(connectorType.Visibility.ToString()) }, + { Constants.XMsMediaKind, new OpenApiString(connectorType.MediaKind.ToString()) } + } + }; + + ISwaggerSchema schema = new SwaggerSchema(connectorType.Schema) + { + Enum = optionSet.EnumType.ValueTree.GetPairs().Select(kvp => new OpenApiDouble((double)kvp.Value.Object) as IOpenApiAny).ToList() + }; + + OpenApiArray array = new OpenApiArray(); + array.AddRange(optionSet.EnumType.ValueTree.GetPairs().Select(kvp => new OpenApiString(kvp.Key))); + + schema.Extensions.Add(new KeyValuePair(Constants.XMsEnumDisplayName, array)); + + // For now, we keep the original formula type (number/string/bool...) + return new ConnectorType(schema, SwaggerParameter.New(openApiParameter), connectorType.FormulaType /* optionSet.FormulaType */); + } + + /// + /// Dynamic intellisense on return value. + /// + /// Known parameters. + /// Connector context. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, with {LogKnownParameters(knownParameters)}"); + ConnectorType connectorType = await GetConnectorReturnTypeAsync(knownParameters, runtimeContext, new CallCounter() { CallsLeft = 1 }, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, returning {nameof(GetConnectorTypeAsync)}, with {LogConnectorType(connectorType)}"); + return connectorType; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, Context {LogKnownParameters(knownParameters)}, {LogException(ex)}"); + throw; + } + } + + /// + /// Dynamic intellisense on return value. + /// + /// Known parameters. + /// Connector context. + /// Max number of recursive network calls. + /// Cancellation token. + /// Formula Type determined by dynamic Intellisense. + public async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, int maxCalls, CancellationToken cancellationToken) + { + return await GetConnectorReturnTypeAsync(knownParameters, runtimeContext, new CallCounter() { CallsLeft = maxCalls }, cancellationToken).ConfigureAwait(false); + } + + internal async Task GetConnectorReturnTypeAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, CallCounter maxCalls, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, with {LogKnownParameters(knownParameters)} and callsLeft {maxCalls.CallsLeft}"); + ConnectorType connectorType = await GetConnectorTypeAsync(knownParameters, ReturnParameterType, runtimeContext, maxCalls, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, returning {nameof(GetConnectorTypeAsync)}, with {LogConnectorType(connectorType)}"); + return connectorType; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(GetConnectorReturnTypeAsync))}, Context {LogKnownParameters(knownParameters)}, {LogException(ex)}"); + throw; + } + } + + /// + /// Generates a Power Fx expression that will invoke this function with the given parameters. + /// + /// Parameters. + /// Power Fx expression. + public string GetExpression(ConnectorParameters parameters) + { + if (!parameters.IsCompleted) + { + return null; + } + + StringBuilder sb = new StringBuilder($@"{Namespace}.{Name}({string.Join(", ", parameters.ParametersWithSuggestions.Take(Math.Min(parameters.ParametersWithSuggestions.Length, ArityMax - 1)).Select(p => p.Value.ToExpression()))}"); + + if (parameters.ParametersWithSuggestions.Length > ArityMax - 1) + { + ConnectorParameterWithSuggestions lastParam = parameters.ParametersWithSuggestions.Last(); + sb.Append($@", {{ "); + + List<(string name, FormulaValue fv)> nameValueAssociations = lastParam.ParameterNames.Zip(lastParam.Values, (name, fv) => (name, fv)).ToList(); + int i = 0; + + foreach ((string name, FormulaValue fv) in nameValueAssociations) + { + sb.Append(name); + sb.Append(": "); + fv.ToExpression(sb, new FormulaValueSerializerSettings()); + + if (++i != nameValueAssociations.Count) + { + sb.Append(", "); + } + } + + sb.Append(" }"); + } + + sb.Append(")"); + + return sb.ToString(); + } + + /// + /// Call connector function. + /// + /// Arguments. + /// RuntimeConnectorContext. + /// Cancellation token. + /// Function result. + public Task InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + return InvokeAsync(arguments, runtimeContext, null, cancellationToken); + } + + /// + /// Call connector function. + /// + /// Arguments. + /// RuntimeConnectorContext. + /// The output type that should be used during output parsing. + /// Cancellation token. + /// Function result. + public async Task InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(InvokeAsync))}, with {LogArguments(arguments)}"); + FormulaValue formulaValue = await InvokeInternalAsync(arguments, runtimeContext, outputTypeOverride, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(InvokeAsync))}, returning from {nameof(InvokeInternalAsync)}, with {LogFormulaValue(formulaValue)}"); + return formulaValue; + } + catch (Exception ex) + { + runtimeContext.ExecutionLogger?.LogException(ex, $"Exception in {this.LogFunction(nameof(InvokeAsync))}, Context {LogArguments(arguments)}, {LogException(ex)}"); + throw; + } + } + + internal Task InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + return InvokeInternalAsync(arguments, runtimeContext, null, cancellationToken); + } + + internal async Task InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + EnsureInitialized(); + runtimeContext.ExecutionLogger?.LogDebug($"Entering in {this.LogFunction(nameof(InvokeInternalAsync))}, with {LogArguments(arguments)}"); + + if (!IsSupported) + { + throw new InvalidOperationException($"In namespace {Namespace}, function {Name} is not supported."); + } + + FormulaValue ev = arguments.Where(arg => arg is ErrorValue).FirstOrDefault(); + if (ev != null) + { + return ev; + } + + BaseRuntimeConnectorContext context = ReturnParameterType.Binary ? runtimeContext.WithRawResults() : runtimeContext; + ScopedHttpFunctionInvoker invoker = new ScopedHttpFunctionInvoker(DPath.Root.Append(DName.MakeValid(Namespace, out _)), Name, Namespace, new HttpFunctionInvoker(this, context), context.ThrowOnError); + FormulaValue result = await invoker.InvokeAsync(arguments, context, outputTypeOverride, cancellationToken).ConfigureAwait(false); + FormulaValue formulaValue = await PostProcessResultAsync(result, runtimeContext, invoker, cancellationToken).ConfigureAwait(false); + + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(InvokeInternalAsync))}, returning {LogFormulaValue(formulaValue)}"); + return formulaValue; + } + + private async Task PostProcessResultAsync(FormulaValue result, BaseRuntimeConnectorContext runtimeContext, ScopedHttpFunctionInvoker invoker, CancellationToken cancellationToken) + { + ExpressionError er = null; + + if (result is ErrorValue ev && (er = ev.Errors.FirstOrDefault(e => e.Kind == ErrorKind.Network || e.Kind == ErrorKind.InvalidJSON || e.Kind == ErrorKind.InvalidArgument)) != null) + { + runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(PostProcessResultAsync))}, ErrorValue is returned with {er.Message}"); + ExpressionError newError = er is HttpExpressionError her + ? new HttpExpressionError(her.StatusCode) { Kind = er.Kind, Severity = er.Severity, Message = $"{DPath.Root.Append(new DName(Namespace)).ToDottedSyntax()}.{Name} failed: {er.Message}" } + : new ExpressionError() { Kind = er.Kind, Severity = er.Severity, Message = $"{DPath.Root.Append(new DName(Namespace)).ToDottedSyntax()}.{Name} failed: {er.Message}" }; result = FormulaValue.NewError(newError, ev.Type); - } - - if (IsPageable && result is RecordValue rv) - { - FormulaValue pageLink = rv.GetField(PageLink); - string nextLink = (pageLink as StringValue)?.Value; - - // If there is no next link, we'll return a "normal" RecordValue as no paging is needed - if (!string.IsNullOrEmpty(nextLink)) - { - result = new PagedRecordValue(rv, async () => await GetNextPageAsync(nextLink, runtimeContext, invoker, cancellationToken).ConfigureAwait(false), ConnectorSettings.MaxRows, cancellationToken); - } - } - - if (ReturnParameterType.FormulaType is BlobType bt && result is StringValue str) - { - result = FormulaValue.NewBlob(str.Value, ReturnParameterType.Schema.Format == "byte"); - } - - return result; - } - - // Can return 3 possible FormulaValues - // - PagesRecordValue if the next page has a next link - // - RecordValue if there is no next link - // - ErrorValue - private async Task GetNextPageAsync(string nextLink, BaseRuntimeConnectorContext runtimeContext, ScopedHttpFunctionInvoker invoker, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetNextPageAsync))}, getting next page"); - - if (!IsSupported) - { - throw new InvalidOperationException($"In namespace {Namespace}, function {Name} is not supported."); - } - - FormulaValue result = await invoker.InvokeAsync(nextLink, runtimeContext, cancellationToken).ConfigureAwait(false); - result = await PostProcessResultAsync(result, runtimeContext, invoker, cancellationToken).ConfigureAwait(false); - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetNextPageAsync))} with {LogFormulaValue(result)}"); - return result; - } - - private static JsonElement ExtractFromJson(StringValue sv, string location) - { - if (sv == null || string.IsNullOrEmpty(sv.Value)) - { - return default; - } - - return ExtractFromJson(JsonDocument.Parse(sv.Value).RootElement, location); - } - - private static JsonElement ExtractFromJson(StringValue sv, string location, out string name, out string displayName) - { - if (sv == null || string.IsNullOrEmpty(sv.Value)) - { - name = displayName = null; - return default; - } - - JsonElement jsonRoot = JsonDocument.Parse(sv.Value).RootElement; - JsonElement je = ExtractFromJson(jsonRoot, location); - - name = displayName = null; - - if (jsonRoot.ValueKind == JsonValueKind.Object) - { - JsonElement.ObjectEnumerator objectEnumerator = jsonRoot.EnumerateObject(); - name = SafeGetProperty(objectEnumerator, "name"); - displayName = SafeGetProperty(objectEnumerator, "title"); - } - - return je; - } - - private static readonly char[] _slash = new char[] { '/' }; - - private static JsonElement ExtractFromJson(JsonElement je, string location) - { - foreach (string vpPart in (location ?? string.Empty).Split(_slash, StringSplitOptions.RemoveEmptyEntries)) - { - if (je.ValueKind != JsonValueKind.Object) - { - return default; - } - - je = je.EnumerateObject().FirstOrDefault(jp => vpPart.Equals(jp.Name, StringComparison.OrdinalIgnoreCase)).Value; - } - - return je; - } - - private static string SafeGetProperty(JsonElement.ObjectEnumerator jsonObjectEnumerator, string propName) - { - JsonElement je = jsonObjectEnumerator.FirstOrDefault(jp => jp.Name.Equals(propName, StringComparison.OrdinalIgnoreCase)).Value; - return je.ValueKind == JsonValueKind.String ? je.GetString() : null; - } - - private async Task GetConnectorSuggestionsFromDynamicSchemaAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicSchema cds, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - FormulaValue[] newParameters = GetArguments(cds, knownParameters, runtimeContext); - - if (newParameters == null) - { - return null; - } - - FormulaValue result = await ConnectorDynamicCallAsync(cds, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); - - if (result is not StringValue sv) - { - runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); - return null; - } - - ConnectorType connectorType = GetConnectorType(cds.ValuePath, sv, ConnectorSettings); - - if (connectorType.HasErrors) - { - runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, with {LogConnectorType(connectorType)}"); - } - else - { - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, with {LogConnectorType(connectorType)}"); - } - - return connectorType; - } - - internal static ConnectorType GetConnectorType(string valuePath, StringValue sv, ConnectorSettings settings) - { - JsonElement je = ExtractFromJson(sv, valuePath); - return GetConnectorTypeInternal(settings, je); - } - - // Only called by ConnectorTable.GetSchema - // Returns a FormulaType with AssociatedDataSources set (done in AddTabularDataSource) + } + + if (IsPageable && result is RecordValue rv) + { + FormulaValue pageLink = rv.GetField(PageLink); + string nextLink = (pageLink as StringValue)?.Value; + + // If there is no next link, we'll return a "normal" RecordValue as no paging is needed + if (!string.IsNullOrEmpty(nextLink)) + { + result = new PagedRecordValue(rv, async () => await GetNextPageAsync(nextLink, runtimeContext, invoker, cancellationToken).ConfigureAwait(false), ConnectorSettings.MaxRows, cancellationToken); + } + } + + if (ReturnParameterType.FormulaType is BlobType bt && result is StringValue str) + { + result = FormulaValue.NewBlob(str.Value, ReturnParameterType.Schema.Format == "byte"); + } + + return result; + } + + // Can return 3 possible FormulaValues + // - PagesRecordValue if the next page has a next link + // - RecordValue if there is no next link + // - ErrorValue + private async Task GetNextPageAsync(string nextLink, BaseRuntimeConnectorContext runtimeContext, ScopedHttpFunctionInvoker invoker, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(GetNextPageAsync))}, getting next page"); + + if (!IsSupported) + { + throw new InvalidOperationException($"In namespace {Namespace}, function {Name} is not supported."); + } + + FormulaValue result = await invoker.InvokeAsync(nextLink, runtimeContext, cancellationToken).ConfigureAwait(false); + result = await PostProcessResultAsync(result, runtimeContext, invoker, cancellationToken).ConfigureAwait(false); + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetNextPageAsync))} with {LogFormulaValue(result)}"); + return result; + } + + private static JsonElement ExtractFromJson(StringValue sv, string location) + { + if (sv == null || string.IsNullOrEmpty(sv.Value)) + { + return default; + } + + return ExtractFromJson(JsonDocument.Parse(sv.Value).RootElement, location); + } + + private static JsonElement ExtractFromJson(StringValue sv, string location, out string name, out string displayName) + { + if (sv == null || string.IsNullOrEmpty(sv.Value)) + { + name = displayName = null; + return default; + } + + JsonElement jsonRoot = JsonDocument.Parse(sv.Value).RootElement; + JsonElement je = ExtractFromJson(jsonRoot, location); + + name = displayName = null; + + if (jsonRoot.ValueKind == JsonValueKind.Object) + { + JsonElement.ObjectEnumerator objectEnumerator = jsonRoot.EnumerateObject(); + name = SafeGetProperty(objectEnumerator, "name"); + displayName = SafeGetProperty(objectEnumerator, "title"); + } + + return je; + } + + private static readonly char[] _slash = new char[] { '/' }; + + private static JsonElement ExtractFromJson(JsonElement je, string location) + { + foreach (string vpPart in (location ?? string.Empty).Split(_slash, StringSplitOptions.RemoveEmptyEntries)) + { + if (je.ValueKind != JsonValueKind.Object) + { + return default; + } + + je = je.EnumerateObject().FirstOrDefault(jp => vpPart.Equals(jp.Name, StringComparison.OrdinalIgnoreCase)).Value; + } + + return je; + } + + private static string SafeGetProperty(JsonElement.ObjectEnumerator jsonObjectEnumerator, string propName) + { + JsonElement je = jsonObjectEnumerator.FirstOrDefault(jp => jp.Name.Equals(propName, StringComparison.OrdinalIgnoreCase)).Value; + return je.ValueKind == JsonValueKind.String ? je.GetString() : null; + } + + private async Task GetConnectorSuggestionsFromDynamicSchemaAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicSchema cds, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FormulaValue[] newParameters = GetArguments(cds, knownParameters, runtimeContext); + + if (newParameters == null) + { + return null; + } + + FormulaValue result = await ConnectorDynamicCallAsync(cds, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); + + if (result is not StringValue sv) + { + runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); + return null; + } + + ConnectorType connectorType = GetConnectorType(cds.ValuePath, sv, ConnectorSettings); + + if (connectorType.HasErrors) + { + runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, with {LogConnectorType(connectorType)}"); + } + else + { + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicSchemaAsync))}, with {LogConnectorType(connectorType)}"); + } + + return connectorType; + } + + internal static ConnectorType GetConnectorType(string valuePath, StringValue sv, ConnectorSettings settings) + { + JsonElement je = ExtractFromJson(sv, valuePath); + return GetConnectorTypeInternal(settings, je); + } + + // Only called by ConnectorTable.GetSchema + // Returns a FormulaType with AssociatedDataSources set (done in AddTabularDataSource) internal static ConnectorType GetCdpTableType(ICdpTableResolver tableResolver, string connectorName, string tableName, string valuePath, StringValue stringValue, ConnectorSettings settings, string datasetName, - out string name, out string displayName, out TableDelegationInfo delegationInfo, out IEnumerable optionSets) - { - // There are some errors when parsing this Json payload but that's not a problem here as we only need x-ms-capabilities parsing to work - OpenApiReaderSettings oars = new OpenApiReaderSettings() { RuleSet = DefaultValidationRuleSet }; + out string name, out string displayName, out TableDelegationInfo delegationInfo, out IEnumerable optionSets) + { + // There are some errors when parsing this Json payload but that's not a problem here as we only need x-ms-capabilities parsing to work + OpenApiReaderSettings oars = new OpenApiReaderSettings() { RuleSet = DefaultValidationRuleSet }; ISwaggerSchema tableSchema = SwaggerSchema.New(new OpenApiStringReader(oars).ReadFragment(stringValue.Value, OpenApi.OpenApiSpecVersion.OpenApi2_0, out OpenApiDiagnostic _)); - - ServiceCapabilities serviceCapabilities = tableSchema.GetTableCapabilities(); + + ServiceCapabilities serviceCapabilities = tableSchema.GetTableCapabilities(); ConnectorPermission tablePermission = tableSchema.GetPermission(); JsonElement jsonElement = ExtractFromJson(stringValue, valuePath, out name, out displayName); @@ -1029,143 +1037,143 @@ internal static ConnectorType GetCdpTableType(ICdpTableResolver tableResolver, s delegationInfo = ((DataSourceInfo)connectorType.FormulaType._type.AssociatedDataSources.First()).DelegationInfo; optionSets = symbolTable.OptionSets.Select(kvp => kvp.Value); - return connectorType; - } - - private static IList GetReferenceEntities(string connectorName, StringValue sv) - { - if (connectorName == "salesforce") - { - // OneToMany relationships that have this table in relation - JsonElement je2 = ExtractFromJson(sv, "referencedEntities", out _, out _); - return je2.Deserialize>() - .Where(kvp => !string.IsNullOrEmpty(kvp.Value.RelationshipName)) - .Select(kvp => new ReferencedEntity() - { - FieldName = kvp.Value.Field, - RelationshipName = kvp.Value.RelationshipName, - TableName = kvp.Value.ChildSObject - }) - .ToList(); - } - - return new List(); - } - - // https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Schema_ChildRelationship.htm - internal class SalesForceReferencedEntity - { - [JsonPropertyName("cascadeDelete")] - public bool CascadeDelete { get; set; } - - [JsonPropertyName("childSObject")] - public string ChildSObject { get; set; } - - [JsonPropertyName("deprecatedAndHidden")] - public bool DeprecatedAndHidden { get; set; } - - [JsonPropertyName("field")] - public string Field { get; set; } - - // ManyToMany - [JsonPropertyName("junctionIdListNames")] - public IList JunctionIdListNames { get; set; } - - [JsonPropertyName("junctionReferenceTo")] - public IList JunctionReferenceTo { get; set; } - - [JsonPropertyName("relationshipName")] - public string RelationshipName { get; set; } - - [JsonPropertyName("restrictedDelete")] - public bool RestrictedDelete { get; set; } - } - - private static ConnectorType GetConnectorTypeInternal(ConnectorSettings settings, JsonElement je) - { - OpenApiReaderSettings oars = new OpenApiReaderSettings() { RuleSet = DefaultValidationRuleSet }; - OpenApiSchema schema = new OpenApiStringReader(oars).ReadFragment(je.ToString(), OpenApi.OpenApiSpecVersion.OpenApi2_0, out OpenApiDiagnostic diag); - - return new ConnectorType(SwaggerSchema.New(schema), settings); - } - - private async Task GetConnectorSuggestionsFromDynamicPropertyAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicProperty cdp, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - FormulaValue[] newParameters = GetArguments(cdp, knownParameters, runtimeContext); - - if (newParameters == null) - { - return null; - } - - FormulaValue result = await ConnectorDynamicCallAsync(cdp, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); - - if (result is not StringValue sv) - { - runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); - return null; - } - - ConnectorType connectorType = GetConnectorType(cdp.ItemValuePath, sv, ConnectorSettings); - - if (connectorType.HasErrors) - { - runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, with {LogConnectorType(connectorType)}"); - } - else - { - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, with {LogConnectorType(connectorType)}"); - } - - return connectorType; - } - - internal static ValidationRuleSet DefaultValidationRuleSet - { - get - { - IList rules = ValidationRuleSet.GetDefaultRuleSet().Rules; - - // OpenApiComponentsRules.KeyMustBeRegularExpression is the only rule with this type - var keyMustBeRegularExpression = rules.First(r => r.GetType() == typeof(ValidationRule)); - rules.Remove(keyMustBeRegularExpression); - - return new ValidationRuleSet(rules); - } - } - - private async Task GetConnectorSuggestionsFromDynamicValueAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicValue cdv, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - FormulaValue[] newParameters = GetArguments(cdv, knownParameters, runtimeContext); - - if (newParameters == null) - { - return null; - } - - FormulaValue result = await ConnectorDynamicCallAsync(cdv, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); - List suggestions = new List(); - - if (result is not StringValue sv) - { - runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicValueAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); - return null; - } - - JsonElement je = ExtractFromJson(sv, cdv.ValueCollection); - - if (je.ValueKind != JsonValueKind.Array && string.IsNullOrEmpty(cdv.ValueCollection)) - { - je = ExtractFromJson(sv, "value"); - } - - if (je.ValueKind == JsonValueKind.Array) - { - foreach (JsonElement jElement in je.EnumerateArray()) - { - JsonElement title = ExtractFromJson(jElement, cdv.ValueTitle); + return connectorType; + } + + private static IList GetReferenceEntities(string connectorName, StringValue sv) + { + if (connectorName == "salesforce") + { + // OneToMany relationships that have this table in relation + JsonElement je2 = ExtractFromJson(sv, "referencedEntities", out _, out _); + return je2.Deserialize>() + .Where(kvp => !string.IsNullOrEmpty(kvp.Value.RelationshipName)) + .Select(kvp => new ReferencedEntity() + { + FieldName = kvp.Value.Field, + RelationshipName = kvp.Value.RelationshipName, + TableName = kvp.Value.ChildSObject + }) + .ToList(); + } + + return new List(); + } + + // https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Schema_ChildRelationship.htm + internal class SalesForceReferencedEntity + { + [JsonPropertyName("cascadeDelete")] + public bool CascadeDelete { get; set; } + + [JsonPropertyName("childSObject")] + public string ChildSObject { get; set; } + + [JsonPropertyName("deprecatedAndHidden")] + public bool DeprecatedAndHidden { get; set; } + + [JsonPropertyName("field")] + public string Field { get; set; } + + // ManyToMany + [JsonPropertyName("junctionIdListNames")] + public IList JunctionIdListNames { get; set; } + + [JsonPropertyName("junctionReferenceTo")] + public IList JunctionReferenceTo { get; set; } + + [JsonPropertyName("relationshipName")] + public string RelationshipName { get; set; } + + [JsonPropertyName("restrictedDelete")] + public bool RestrictedDelete { get; set; } + } + + private static ConnectorType GetConnectorTypeInternal(ConnectorSettings settings, JsonElement je) + { + OpenApiReaderSettings oars = new OpenApiReaderSettings() { RuleSet = DefaultValidationRuleSet }; + OpenApiSchema schema = new OpenApiStringReader(oars).ReadFragment(je.ToString(), OpenApi.OpenApiSpecVersion.OpenApi2_0, out OpenApiDiagnostic diag); + + return new ConnectorType(SwaggerSchema.New(schema), settings); + } + + private async Task GetConnectorSuggestionsFromDynamicPropertyAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicProperty cdp, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FormulaValue[] newParameters = GetArguments(cdp, knownParameters, runtimeContext); + + if (newParameters == null) + { + return null; + } + + FormulaValue result = await ConnectorDynamicCallAsync(cdp, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); + + if (result is not StringValue sv) + { + runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); + return null; + } + + ConnectorType connectorType = GetConnectorType(cdp.ItemValuePath, sv, ConnectorSettings); + + if (connectorType.HasErrors) + { + runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, with {LogConnectorType(connectorType)}"); + } + else + { + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicPropertyAsync))}, with {LogConnectorType(connectorType)}"); + } + + return connectorType; + } + + internal static ValidationRuleSet DefaultValidationRuleSet + { + get + { + IList rules = ValidationRuleSet.GetDefaultRuleSet().Rules; + + // OpenApiComponentsRules.KeyMustBeRegularExpression is the only rule with this type + var keyMustBeRegularExpression = rules.First(r => r.GetType() == typeof(ValidationRule)); + rules.Remove(keyMustBeRegularExpression); + + return new ValidationRuleSet(rules); + } + } + + private async Task GetConnectorSuggestionsFromDynamicValueAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicValue cdv, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FormulaValue[] newParameters = GetArguments(cdv, knownParameters, runtimeContext); + + if (newParameters == null) + { + return null; + } + + FormulaValue result = await ConnectorDynamicCallAsync(cdv, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); + List suggestions = new List(); + + if (result is not StringValue sv) + { + runtimeContext.ExecutionLogger?.LogError($"{this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicValueAsync))}, result isn't a StringValue but {LogFormulaValue(result)}"); + return null; + } + + JsonElement je = ExtractFromJson(sv, cdv.ValueCollection); + + if (je.ValueKind != JsonValueKind.Array && string.IsNullOrEmpty(cdv.ValueCollection)) + { + je = ExtractFromJson(sv, "value"); + } + + if (je.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement jElement in je.EnumerateArray()) + { + JsonElement title = ExtractFromJson(jElement, cdv.ValueTitle); JsonElement value = ExtractFromJson(jElement, cdv.ValuePath); // Note: Some connectors have misalignment between the Swagger definition and the actual response, caused suggestion API @@ -1185,43 +1193,43 @@ private async Task GetConnectorSuggestionsFromDyna } } - if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined) - { - continue; - } - - suggestions.Add(new ConnectorSuggestion(FormulaValueJSON.FromJson(value), title.ToString())); - } - } - - runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicValueAsync))}, with {suggestions.Count} suggestions"); - return new ConnectorEnhancedSuggestions(SuggestionMethod.DynamicValue, suggestions); - } - - private async Task GetConnectorSuggestionsFromDynamicListAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicList cdl, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - FormulaValue[] newParameters = GetArguments(cdl, knownParameters, runtimeContext); - - if (newParameters == null) - { - return null; - } - - FormulaValue result = await ConnectorDynamicCallAsync(cdl, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); - List suggestions = new List(); - - if (result is not StringValue sv) - { - runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicListAsync))} with null, result isn't a StringValue but {LogFormulaValue(result)}"); - return null; - } - - JsonElement je = ExtractFromJson(sv, cdl.ItemPath); - - foreach (JsonElement jElement in je.EnumerateArray()) - { - JsonElement title = ExtractFromJson(jElement, cdl.ItemTitlePath); + if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined) + { + continue; + } + + suggestions.Add(new ConnectorSuggestion(FormulaValueJSON.FromJson(value), title.ToString())); + } + } + + runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicValueAsync))}, with {suggestions.Count} suggestions"); + return new ConnectorEnhancedSuggestions(SuggestionMethod.DynamicValue, suggestions); + } + + private async Task GetConnectorSuggestionsFromDynamicListAsync(NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext, ConnectorDynamicList cdl, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + FormulaValue[] newParameters = GetArguments(cdl, knownParameters, runtimeContext); + + if (newParameters == null) + { + return null; + } + + FormulaValue result = await ConnectorDynamicCallAsync(cdl, newParameters, runtimeContext, cancellationToken).ConfigureAwait(false); + List suggestions = new List(); + + if (result is not StringValue sv) + { + runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicListAsync))} with null, result isn't a StringValue but {LogFormulaValue(result)}"); + return null; + } + + JsonElement je = ExtractFromJson(sv, cdl.ItemPath); + + foreach (JsonElement jElement in je.EnumerateArray()) + { + JsonElement title = ExtractFromJson(jElement, cdl.ItemTitlePath); JsonElement value = ExtractFromJson(jElement, cdl.ItemValuePath); if (ConnectorSettings?.AllowSuggestionMappingFallback == true) @@ -1235,231 +1243,230 @@ private async Task GetConnectorSuggestionsFromDyna { value = ExtractFromJson(jElement, "value"); } - } - - if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined) - { - continue; - } - - suggestions.Add(new ConnectorSuggestion(FormulaValueJSON.FromJson(value), title.ToString())); - } - - runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicListAsync))}, returning {suggestions.Count} suggestions"); - return new ConnectorEnhancedSuggestions(SuggestionMethod.DynamicList, suggestions); - } - - private async Task ConnectorDynamicCallAsync(ConnectorDynamicApi dynamicApi, FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - EnsureConnectorFunction(dynamicApi, GlobalContext.FunctionList); - if (dynamicApi.ConnectorFunction == null) - { - runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(ConnectorDynamicCallAsync))}, {nameof(dynamicApi.ConnectorFunction)} is null"); - return null; - } - - return await dynamicApi.ConnectorFunction.InvokeInternalAsync(arguments, runtimeContext.WithRawResults(), cancellationToken).ConfigureAwait(false); - } - - private void EnsureInitialized() - { - _internals ??= Initialize(); - } - - private T EnsureConnectorFunction(T dynamicApi, IReadOnlyList functionList) - where T : ConnectorDynamicApi - { - if (dynamicApi == null) - { - return dynamicApi; - } - - dynamicApi.ConnectorFunction ??= functionList.FirstOrDefault(cf => dynamicApi.OperationId == cf.Name); - return dynamicApi; - } - - // Only used by ConnectorTexlFunction - private DType[] GetParamTypes() - { - if (RequiredParameters == null) - { - return Array.Empty(); - } - - IEnumerable parameterTypes = RequiredParameters.Select(parameter => parameter.FormulaType._type); - if (OptionalParameters.Length != 0) - { - DType optionalParameterType = DType.CreateRecord(OptionalParameters.Select(cpi => new TypedName(cpi.FormulaType._type, new DName(cpi.Name)))); - optionalParameterType.AreFieldsOptional = true; - - _parameterTypes = parameterTypes.Append(optionalParameterType).ToArray(); - return _parameterTypes; - } - - _parameterTypes = parameterTypes.ToArray(); - return _parameterTypes; - } - - // This method only returns null when an error occurs - // Otherwise it will return an empty array - private FormulaValue[] GetArguments(ConnectorDynamicApi dynamicApi, NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext) - { - List arguments = new List(); - - ConnectorFunction functionToBeCalled = EnsureConnectorFunction(dynamicApi, GlobalContext.FunctionList).ConnectorFunction; - - foreach (ConnectorParameter connectorParameter in functionToBeCalled.RequiredParameters) - { - string requiredParameterName = connectorParameter.Name; - - // TODO: properly implement the ability to reference child properties instead of simplistic check "GetLastPart(kvp.Key) == requiredParameterName" - // doc: https://learn.microsoft.com/en-us/connectors/custom-connectors/openapi-extensions - // e.g. make this example working: - // "destinationInputParam1/property1": { - // "parameterReference": "sourceInputParam1/property1" - // } - if (dynamicApi.ParameterMap.FirstOrDefault(kvp => kvp.Value is StaticConnectorExtensionValue && GetLastPart(kvp.Key) == requiredParameterName).Value is StaticConnectorExtensionValue sValue) - { - arguments.Add(sValue.Value); - continue; - } - - KeyValuePair dValue = dynamicApi.ParameterMap.FirstOrDefault(kvp => kvp.Value is DynamicConnectorExtensionValue dv && GetLastPart(kvp.Key) == requiredParameterName); - string[] referenceList = ((DynamicConnectorExtensionValue)dValue.Value).Reference.Split('/'); - - var parameterToUse = knownParameters.FirstOrDefault(nv => nv.Name == referenceList.First())?.Value; - for (int i = 1; i < referenceList.Length && parameterToUse != null; i++) - { - if (parameterToUse is RecordValue recordValue) - { - parameterToUse = recordValue.GetField(referenceList[i]); - } - else - { - runtimeContext.ExecutionLogger?.LogWarning($"Provided parameter is expected to be a record but is {parameterToUse.Type._type.ToAnonymousString()}"); - return null; - } - } - - if (parameterToUse == null || parameterToUse.IsBlank()) - { - runtimeContext.ExecutionLogger?.LogWarning($"Missing required property to run suggestions"); - return null; - } - - arguments.Add(parameterToUse); - } - - return arguments.ToArray(); - } - - private string GetLastPart(string str) => str.Split('/').Last(); - - private ConnectorParameterInternals Initialize() - { - // Hidden-Required parameters exist in the following conditions: - // 1. required parameter - // 2. has default value - // 3. is marked "internal" in schema extension named "x-ms-visibility" - List requiredParameters = new (); - List hiddenRequiredParameters = new (); - List optionalParameters = new (); - - // parameters used in ConnectorParameterInternals - Dictionary parameterDefaultValues = new (); - Dictionary openApiBodyParameters = new (); - string bodySchemaReferenceId = null; - bool schemaLessBody = false; - bool fatalError = false; - bool specialBodyHandling = false; - string contentType = OpenApiExtensions.ContentType_ApplicationJson; - ConnectorErrors errorsAndWarnings = new ConnectorErrors(); - - foreach (OpenApiParameter parameter in Operation.Parameters) - { - bool hiddenRequired = false; - - if (parameter == null) - { - errorsAndWarnings.AddError($"OpenApiParameter is null, this swagger file is probably containing errors"); - fatalError = true; - break; - } - - if (parameter.IsInternal()) - { - if (parameter.Required) - { - if (parameter.Schema.Default == null && (parameter.Name == "connectionId" || !ConnectorSettings.ExposeInternalParamsWithoutDefaultValue)) - { - // Ex: connectionId - continue; - } + } + + if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined) + { + continue; + } + + suggestions.Add(new ConnectorSuggestion(FormulaValueJSON.FromJson(value), title.ToString())); + } + + runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(GetConnectorSuggestionsFromDynamicListAsync))}, returning {suggestions.Count} suggestions"); + return new ConnectorEnhancedSuggestions(SuggestionMethod.DynamicList, suggestions); + } + + private async Task ConnectorDynamicCallAsync(ConnectorDynamicApi dynamicApi, FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + EnsureConnectorFunction(dynamicApi, GlobalContext.FunctionList); + if (dynamicApi.ConnectorFunction == null) + { + runtimeContext.ExecutionLogger?.LogError($"Exiting {this.LogFunction(nameof(ConnectorDynamicCallAsync))}, {nameof(dynamicApi.ConnectorFunction)} is null"); + return null; + } + + return await dynamicApi.ConnectorFunction.InvokeInternalAsync(arguments, runtimeContext.WithRawResults(), cancellationToken).ConfigureAwait(false); + } + + private void EnsureInitialized() + { + _internals ??= Initialize(); + } + + private T EnsureConnectorFunction(T dynamicApi, IReadOnlyList functionList) + where T : ConnectorDynamicApi + { + if (dynamicApi == null) + { + return dynamicApi; + } + + dynamicApi.ConnectorFunction ??= functionList.FirstOrDefault(cf => dynamicApi.OperationId == cf.Name); + return dynamicApi; + } + + // Only used by ConnectorTexlFunction + private DType[] GetParamTypes() + { + if (RequiredParameters == null) + { + return Array.Empty(); + } + + IEnumerable parameterTypes = RequiredParameters.Select(parameter => parameter.FormulaType._type); + if (OptionalParameters.Length != 0) + { + DType optionalParameterType = DType.CreateRecord(OptionalParameters.Select(cpi => new TypedName(cpi.FormulaType._type, new DName(cpi.Name)))); + optionalParameterType.AreFieldsOptional = true; + + _parameterTypes = parameterTypes.Append(optionalParameterType).ToArray(); + return _parameterTypes; + } + + _parameterTypes = parameterTypes.ToArray(); + return _parameterTypes; + } + + // This method only returns null when an error occurs + // Otherwise it will return an empty array + private FormulaValue[] GetArguments(ConnectorDynamicApi dynamicApi, NamedValue[] knownParameters, BaseRuntimeConnectorContext runtimeContext) + { + List arguments = new List(); + + ConnectorFunction functionToBeCalled = EnsureConnectorFunction(dynamicApi, GlobalContext.FunctionList).ConnectorFunction; + + foreach (ConnectorParameter connectorParameter in functionToBeCalled.RequiredParameters) + { + string requiredParameterName = connectorParameter.Name; + + // TODO: properly implement the ability to reference child properties instead of simplistic check "GetLastPart(kvp.Key) == requiredParameterName" + // doc: https://learn.microsoft.com/en-us/connectors/custom-connectors/openapi-extensions + // e.g. make this example working: + // "destinationInputParam1/property1": { + // "parameterReference": "sourceInputParam1/property1" + // } + if (dynamicApi.ParameterMap.FirstOrDefault(kvp => kvp.Value is StaticConnectorExtensionValue && GetLastPart(kvp.Key) == requiredParameterName).Value is StaticConnectorExtensionValue sValue) + { + arguments.Add(sValue.Value); + continue; + } + + KeyValuePair dValue = dynamicApi.ParameterMap.FirstOrDefault(kvp => kvp.Value is DynamicConnectorExtensionValue dv && GetLastPart(kvp.Key) == requiredParameterName); + string[] referenceList = ((DynamicConnectorExtensionValue)dValue.Value).Reference.Split('/'); + + var parameterToUse = knownParameters.FirstOrDefault(nv => nv.Name == referenceList.First())?.Value; + for (int i = 1; i < referenceList.Length && parameterToUse != null; i++) + { + if (parameterToUse is RecordValue recordValue) + { + parameterToUse = recordValue.GetField(referenceList[i]); + } + else + { + runtimeContext.ExecutionLogger?.LogWarning($"Provided parameter is expected to be a record but is {parameterToUse.Type._type.ToAnonymousString()}"); + return null; + } + } + + if (parameterToUse == null || parameterToUse.IsBlank()) + { + runtimeContext.ExecutionLogger?.LogWarning($"Missing required property to run suggestions"); + return null; + } + + arguments.Add(parameterToUse); + } + + return arguments.ToArray(); + } + + private string GetLastPart(string str) => str.Split('/').Last(); + + private ConnectorParameterInternals Initialize() + { + // Hidden-Required parameters exist in the following conditions: + // 1. required parameter + // 2. has default value + // 3. is marked "internal" in schema extension named "x-ms-visibility" + List requiredParameters = new (); + List hiddenRequiredParameters = new (); + List optionalParameters = new (); + + // parameters used in ConnectorParameterInternals + Dictionary parameterDefaultValues = new (); + Dictionary openApiBodyParameters = new (); + string bodySchemaReferenceId = null; + bool schemaLessBody = false; + bool fatalError = false; + string contentType = OpenApiExtensions.ContentType_ApplicationJson; + ConnectorErrors errorsAndWarnings = new ConnectorErrors(); + + foreach (OpenApiParameter parameter in Operation.Parameters) + { + bool hiddenRequired = false; + + if (parameter == null) + { + errorsAndWarnings.AddError($"OpenApiParameter is null, this swagger file is probably containing errors"); + fatalError = true; + break; + } + + if (parameter.IsInternal()) + { + if (parameter.Required) + { + if (parameter.Schema.Default == null && (parameter.Name == "connectionId" || !ConnectorSettings.ExposeInternalParamsWithoutDefaultValue)) + { + // Ex: connectionId + continue; + } if (parameter.Schema.Default != null) { // Ex: Api-Version hiddenRequired = true; } - } - else if (ConnectorSettings.Compatibility.ExcludeInternals()) - { - continue; - } - } - - if (!VerifyCanHandle(parameter.In)) - { - return null; - } - + } + else if (ConnectorSettings.Compatibility.ExcludeInternals()) + { + continue; + } + } + + if (!VerifyCanHandle(parameter.In)) + { + return null; + } + ConnectorParameter connectorParameter = errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(parameter, ConnectorSettings)); - if (connectorParameter.HiddenRecordType != null) - { - errorsAndWarnings.AddError("[Internal error] Unexpected HiddenRecordType non-null value"); - fatalError = true; - } - - if (SwaggerSchema.New(parameter.Schema).TryGetDefaultValue(connectorParameter.FormulaType, out FormulaValue defaultValue, errorsAndWarnings)) - { - parameterDefaultValues[parameter.Name] = (connectorParameter.ConnectorType.IsRequired, defaultValue, connectorParameter.FormulaType._type); - } - - List parameterList = !parameter.Required ? optionalParameters : hiddenRequired ? hiddenRequiredParameters : requiredParameters; - parameterList.Add(connectorParameter); - } - - if (!fatalError) - { - if (Operation.RequestBody != null) - { - OpenApiRequestBody requestBody = Operation.RequestBody; - string bodyName = requestBody.GetBodyName(); - - if (requestBody.Content != null && requestBody.Content.Any()) - { - (string cnt, OpenApiMediaType mediaType) = requestBody.Content.GetContentTypeAndSchema(); - - if (!string.IsNullOrEmpty(contentType) && mediaType != null) - { - OpenApiSchema bodySchema = mediaType.Schema; - contentType = cnt; - bodySchemaReferenceId = bodySchema?.Reference?.Id; - - // Additional properties are ignored for now - if (bodySchema.AnyOf.Any() || bodySchema.Not != null || (bodySchema.Items != null && bodySchema.Type != "array")) - { - errorsAndWarnings.AddError("[Body] OpenApiSchema is not supported - AnyOf, Not, AdditionalProperties or Items not array"); - } - else if (bodySchema.AllOf.Any() || bodySchema.Properties.Any()) - { - foreach (KeyValuePair bodyProperty in bodySchema.Properties) - { - OpenApiSchema bodyPropertySchema = bodyProperty.Value; - string bodyPropertyName = bodyProperty.Key; + if (connectorParameter.HiddenRecordType != null) + { + errorsAndWarnings.AddError("[Internal error] Unexpected HiddenRecordType non-null value"); + fatalError = true; + } + + if (SwaggerSchema.New(parameter.Schema).TryGetDefaultValue(connectorParameter.FormulaType, out FormulaValue defaultValue, errorsAndWarnings)) + { + parameterDefaultValues[parameter.Name] = (connectorParameter.ConnectorType.IsRequired, defaultValue, connectorParameter.FormulaType._type); + } + + List parameterList = !parameter.Required ? optionalParameters : hiddenRequired ? hiddenRequiredParameters : requiredParameters; + parameterList.Add(connectorParameter); + } + + if (!fatalError) + { + if (Operation.RequestBody != null) + { + OpenApiRequestBody requestBody = Operation.RequestBody; + string bodyName = requestBody.GetBodyName(); + + if (requestBody.Content != null && requestBody.Content.Any()) + { + (string cnt, OpenApiMediaType mediaType) = requestBody.Content.GetContentTypeAndSchema(); + + if (!string.IsNullOrEmpty(contentType) && mediaType != null) + { + OpenApiSchema bodySchema = mediaType.Schema; + contentType = cnt; + bodySchemaReferenceId = bodySchema?.Reference?.Id; + + // Additional properties are ignored for now + if (bodySchema.AnyOf.Any() || bodySchema.Not != null || (bodySchema.Items != null && bodySchema.Type != "array")) + { + errorsAndWarnings.AddError("[Body] OpenApiSchema is not supported - AnyOf, Not, AdditionalProperties or Items not array"); + } + else if (bodySchema.AllOf.Any() || bodySchema.Properties.Any()) + { + foreach (KeyValuePair bodyProperty in bodySchema.Properties) + { + OpenApiSchema bodyPropertySchema = bodyProperty.Value; + string bodyPropertyName = bodyProperty.Key; bool bodyPropertyHiddenRequired = false; // Power Apps has a special handling for the body in this case @@ -1470,218 +1477,218 @@ private ConnectorParameterInternals Initialize() bodySchema.Properties.Count == 1) { bodyPropertyName = bodyName; - specialBodyHandling = true; + _specialBodyHandling = true; } - bool bodyPropertyRequired = bodySchema.Required.Contains(bodyPropertyName) || (ConnectorSettings.UseItemDynamicPropertiesSpecialHandling && requestBody.Required); - - if (bodyPropertySchema.IsInternal()) - { - if (bodyPropertyRequired) - { - if (bodyPropertySchema.Default == null && !ConnectorSettings.ExposeInternalParamsWithoutDefaultValue) - { - continue; + bool bodyPropertyRequired = bodySchema.Required.Contains(bodyPropertyName) || (ConnectorSettings.UseItemDynamicPropertiesSpecialHandling && requestBody.Required); + + if (bodyPropertySchema.IsInternal()) + { + if (bodyPropertyRequired) + { + if (bodyPropertySchema.Default == null && !ConnectorSettings.ExposeInternalParamsWithoutDefaultValue) + { + continue; } if (bodyPropertySchema.Default != null) { bodyPropertyHiddenRequired = !ConnectorSettings.Compatibility.IsPowerAppsCompliant() || !requestBody.Required; - } - } - else if (ConnectorSettings.Compatibility.ExcludeInternals()) - { - continue; - } - } - - OpenApiParameter bodyParameter = new OpenApiParameter() { Name = bodyPropertyName, Schema = bodyPropertySchema, Description = requestBody.Description, Required = bodyPropertyRequired, Extensions = bodyPropertySchema.Extensions }; + } + } + else if (ConnectorSettings.Compatibility.ExcludeInternals()) + { + continue; + } + } + + OpenApiParameter bodyParameter = new OpenApiParameter() { Name = bodyPropertyName, Schema = bodyPropertySchema, Description = requestBody.Description, Required = bodyPropertyRequired, Extensions = bodyPropertySchema.Extensions }; ConnectorParameter bodyConnectorParameter2 = errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(bodyParameter, requestBody, ConnectorSettings)); - openApiBodyParameters.Add(bodyConnectorParameter2, OpenApiExtensions.TryGetOpenApiValue(bodyConnectorParameter2.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); - - if (bodyConnectorParameter2.HiddenRecordType != null) + openApiBodyParameters.Add(bodyConnectorParameter2, OpenApiExtensions.TryGetOpenApiValue(bodyConnectorParameter2.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); + + if (bodyConnectorParameter2.HiddenRecordType != null) { - hiddenRequiredParameters.Add(errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(bodyParameter, true, ConnectorSettings))); - } - - List parameterList = !bodyPropertyRequired ? optionalParameters : bodyPropertyHiddenRequired ? hiddenRequiredParameters : requiredParameters; - parameterList.Add(bodyConnectorParameter2); - } - } - else - { - schemaLessBody = true; - - if (bodySchema.Type == "string" && bodySchema.Format == "binary") - { - // Blob - In Power Apps, when the body parameter is of type "binary", the name of the parameter becomes "file" - // ServiceConfigParser.cs, see DefaultBinaryRequestBodyParameterName reference - bodyName = "file"; - } - - OpenApiParameter bodyParameter2 = new OpenApiParameter() { Name = bodyName, Schema = bodySchema, Description = requestBody.Description, Required = requestBody.Required, Extensions = bodySchema.Extensions }; + hiddenRequiredParameters.Add(errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(bodyParameter, true, ConnectorSettings))); + } + + List parameterList = !bodyPropertyRequired ? optionalParameters : bodyPropertyHiddenRequired ? hiddenRequiredParameters : requiredParameters; + parameterList.Add(bodyConnectorParameter2); + } + } + else + { + schemaLessBody = true; + + if (bodySchema.Type == "string" && bodySchema.Format == "binary") + { + // Blob - In Power Apps, when the body parameter is of type "binary", the name of the parameter becomes "file" + // ServiceConfigParser.cs, see DefaultBinaryRequestBodyParameterName reference + bodyName = "file"; + } + + OpenApiParameter bodyParameter2 = new OpenApiParameter() { Name = bodyName, Schema = bodySchema, Description = requestBody.Description, Required = requestBody.Required, Extensions = bodySchema.Extensions }; ConnectorParameter bodyConnectorParameter3 = errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(bodyParameter2, requestBody, ConnectorSettings)); - openApiBodyParameters.Add(bodyConnectorParameter3, OpenApiExtensions.TryGetOpenApiValue(bodyConnectorParameter3.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); - - if (bodyConnectorParameter3.HiddenRecordType != null) - { - errorsAndWarnings.AddError("[Internal error] Unexpected HiddenRecordType not-null value for schema-less body"); - } - - List parameterList = requestBody.Required ? requiredParameters : optionalParameters; - parameterList.Add(bodyConnectorParameter3); - } - } - } - else - { - // If the content isn't specified, we will expect Json in the body - contentType = OpenApiExtensions.ContentType_ApplicationJson; - OpenApiSchema bodyParameterSchema = new OpenApiSchema() { Type = "string" }; - - OpenApiParameter bodyParameter3 = new OpenApiParameter() { Name = bodyName, Schema = bodyParameterSchema, Description = "Body", Required = requestBody.Required }; + openApiBodyParameters.Add(bodyConnectorParameter3, OpenApiExtensions.TryGetOpenApiValue(bodyConnectorParameter3.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); + + if (bodyConnectorParameter3.HiddenRecordType != null) + { + errorsAndWarnings.AddError("[Internal error] Unexpected HiddenRecordType not-null value for schema-less body"); + } + + List parameterList = requestBody.Required ? requiredParameters : optionalParameters; + parameterList.Add(bodyConnectorParameter3); + } + } + } + else + { + // If the content isn't specified, we will expect Json in the body + contentType = OpenApiExtensions.ContentType_ApplicationJson; + OpenApiSchema bodyParameterSchema = new OpenApiSchema() { Type = "string" }; + + OpenApiParameter bodyParameter3 = new OpenApiParameter() { Name = bodyName, Schema = bodyParameterSchema, Description = "Body", Required = requestBody.Required }; ConnectorParameter bodyParameter = errorsAndWarnings.AggregateErrorsAndWarnings(new ConnectorParameter(bodyParameter3, requestBody, ConnectorSettings)); - openApiBodyParameters.Add(bodyParameter, OpenApiExtensions.TryGetOpenApiValue(bodyParameter.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); - - List parameterList = requestBody.Required ? requiredParameters : optionalParameters; - parameterList.Add(bodyParameter); - } - } - - // Validate we have no name conflict between required and optional parameters - // In case of conflict, we rename the optional parameter and add _1, _2, etc. until we have no conflict - // We could imagine an API with required param Foo, and optional body params Foo and Foo_1 but this is not considered for now - // Implemented in PA Client in src\Cloud\DocumentServer.Core\Document\Importers\ServiceConfig\RestFunctionDefinitionBuilder.cs at line 1176 - CreateUniqueImpliedParameterName - List requiredParamNames = requiredParameters.Select(rpi => rpi.Name).ToList(); - foreach (ConnectorParameter opi in optionalParameters) - { - string paramName = opi.Name; - - if (requiredParamNames.Contains(paramName)) - { - int i = 0; - string newName; - - do - { - newName = $"{paramName}_{++i}"; - } - while (requiredParamNames.Contains(newName)); - - opi.SetName(newName); - } - } - } - - // Required params are first N params in the final list, "in" parameters first. - // Optional params are fields on a single record argument at the end. - // Hidden required parameters do not count here - _requiredParameters = ConnectorSettings.Compatibility.IsPowerAppsCompliant() ? GetPowerAppsParameterOrder(requiredParameters) : requiredParameters.ToArray(); - _optionalParameters = optionalParameters.ToArray(); - _hiddenRequiredParameters = hiddenRequiredParameters.ToArray(); - _arityMin = _requiredParameters.Length; - _arityMax = _arityMin + (_optionalParameters.Length == 0 ? 0 : 1); - _warnings = new List(); - + openApiBodyParameters.Add(bodyParameter, OpenApiExtensions.TryGetOpenApiValue(bodyParameter.Schema.Default, null, out FormulaValue defaultValue, errorsAndWarnings) ? defaultValue : null); + + List parameterList = requestBody.Required ? requiredParameters : optionalParameters; + parameterList.Add(bodyParameter); + } + } + + // Validate we have no name conflict between required and optional parameters + // In case of conflict, we rename the optional parameter and add _1, _2, etc. until we have no conflict + // We could imagine an API with required param Foo, and optional body params Foo and Foo_1 but this is not considered for now + // Implemented in PA Client in src\Cloud\DocumentServer.Core\Document\Importers\ServiceConfig\RestFunctionDefinitionBuilder.cs at line 1176 - CreateUniqueImpliedParameterName + List requiredParamNames = requiredParameters.Select(rpi => rpi.Name).ToList(); + foreach (ConnectorParameter opi in optionalParameters) + { + string paramName = opi.Name; + + if (requiredParamNames.Contains(paramName)) + { + int i = 0; + string newName; + + do + { + newName = $"{paramName}_{++i}"; + } + while (requiredParamNames.Contains(newName)); + + opi.SetName(newName); + } + } + } + + // Required params are first N params in the final list, "in" parameters first. + // Optional params are fields on a single record argument at the end. + // Hidden required parameters do not count here + _requiredParameters = ConnectorSettings.Compatibility.IsPowerAppsCompliant() ? GetPowerAppsParameterOrder(requiredParameters) : requiredParameters.ToArray(); + _optionalParameters = optionalParameters.ToArray(); + _hiddenRequiredParameters = hiddenRequiredParameters.ToArray(); + _arityMin = _requiredParameters.Length; + _arityMax = _arityMin + (_optionalParameters.Length == 0 ? 0 : 1); + _warnings = new List(); + _returnType = errorsAndWarnings.AggregateErrorsAndWarnings(Operation.GetConnectorReturnType(ConnectorSettings)); - if (IsDeprecated) - { - _warnings.Add(ConnectorStringResources.WarnDeprecatedFunction); - string msg = ErrorUtils.FormatMessage(StringResources.Get(ConnectorStringResources.WarnDeprecatedFunction), null, Name, Namespace); - _configurationLogger?.LogWarning($"{msg}"); - } - - if (errorsAndWarnings.HasErrors) - { - foreach (string error in errorsAndWarnings.Errors) - { - _configurationLogger?.LogError($"Function {Name}: {error}"); - } - - SetUnsupported(string.Join(", ", errorsAndWarnings.Errors)); - } - - if (errorsAndWarnings.HasWarnings) - { - foreach (ErrorResourceKey warning in errorsAndWarnings.Warnings) - { - string msg = ErrorUtils.FormatMessage(StringResources.Get(warning), null, Name, Namespace); - _configurationLogger?.LogWarning($"Function {Name}: {msg}"); - } - - _warnings.AddRange(errorsAndWarnings.Warnings.ToArray()); - } - - return new ConnectorParameterInternals() - { - OpenApiBodyParameters = openApiBodyParameters, - ContentType = contentType, - BodySchemaReferenceId = bodySchemaReferenceId, - ParameterDefaultValues = parameterDefaultValues, + if (IsDeprecated) + { + _warnings.Add(ConnectorStringResources.WarnDeprecatedFunction); + string msg = ErrorUtils.FormatMessage(StringResources.Get(ConnectorStringResources.WarnDeprecatedFunction), null, Name, Namespace); + _configurationLogger?.LogWarning($"{msg}"); + } + + if (errorsAndWarnings.HasErrors) + { + foreach (string error in errorsAndWarnings.Errors) + { + _configurationLogger?.LogError($"Function {Name}: {error}"); + } + + SetUnsupported(string.Join(", ", errorsAndWarnings.Errors)); + } + + if (errorsAndWarnings.HasWarnings) + { + foreach (ErrorResourceKey warning in errorsAndWarnings.Warnings) + { + string msg = ErrorUtils.FormatMessage(StringResources.Get(warning), null, Name, Namespace); + _configurationLogger?.LogWarning($"Function {Name}: {msg}"); + } + + _warnings.AddRange(errorsAndWarnings.Warnings.ToArray()); + } + + return new ConnectorParameterInternals() + { + OpenApiBodyParameters = openApiBodyParameters, + ContentType = contentType, + BodySchemaReferenceId = bodySchemaReferenceId, + ParameterDefaultValues = parameterDefaultValues, SchemaLessBody = schemaLessBody, - SpecialBodyHandling = specialBodyHandling - }; - } - - private ConnectorParameter[] GetPowerAppsParameterOrder(List parameters) - { - List newList = new List(); - - foreach (ConnectorParameter parameter in parameters) - { - if (parameter.Location == ParameterLocation.Path) - { - newList.Add(parameter); - } - } - - foreach (ConnectorParameter parameter in parameters) - { - if (parameter.Location == ParameterLocation.Query) - { - newList.Add(parameter); - } - } - - foreach (ConnectorParameter parameter in parameters) - { - if (parameter.Location == ParameterLocation.Header) - { - newList.Add(parameter); - } - } - - foreach (ConnectorParameter parameter in parameters) - { - if (parameter.Location == null || parameter.Location == ParameterLocation.Cookie) - { - newList.Add(parameter); - } - } - - return newList.ToArray(); - } - - private bool VerifyCanHandle(ParameterLocation? location) - { - switch (location.Value) - { - case ParameterLocation.Path: - case ParameterLocation.Query: - case ParameterLocation.Header: - return true; - - case ParameterLocation.Cookie: - default: - _configurationLogger?.LogError($"{this.LogFunction(nameof(VerifyCanHandle))}, unsupported {location.Value}"); - return false; - } - } - - internal class CallCounter - { - internal int CallsLeft; - } - } -} + SpecialBodyHandling = _specialBodyHandling + }; + } + + private ConnectorParameter[] GetPowerAppsParameterOrder(List parameters) + { + List newList = new List(); + + foreach (ConnectorParameter parameter in parameters) + { + if (parameter.Location == ParameterLocation.Path) + { + newList.Add(parameter); + } + } + + foreach (ConnectorParameter parameter in parameters) + { + if (parameter.Location == ParameterLocation.Query) + { + newList.Add(parameter); + } + } + + foreach (ConnectorParameter parameter in parameters) + { + if (parameter.Location == ParameterLocation.Header) + { + newList.Add(parameter); + } + } + + foreach (ConnectorParameter parameter in parameters) + { + if (parameter.Location == null || parameter.Location == ParameterLocation.Cookie) + { + newList.Add(parameter); + } + } + + return newList.ToArray(); + } + + private bool VerifyCanHandle(ParameterLocation? location) + { + switch (location.Value) + { + case ParameterLocation.Path: + case ParameterLocation.Query: + case ParameterLocation.Header: + return true; + + case ParameterLocation.Cookie: + default: + _configurationLogger?.LogError($"{this.LogFunction(nameof(VerifyCanHandle))}, unsupported {location.Value}"); + return false; + } + } + + internal class CallCounter + { + internal int CallsLeft; + } + } +} diff --git a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs index 4a7cabe87e..6172e1b632 100644 --- a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs +++ b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformConnectorTests.cs @@ -2417,6 +2417,7 @@ public async Task ExchangeOnlineTest2(bool useItemDynamicPropertiesSpecialHandli ConnectorFunction patchItem = functions.First(f => f.Name == "PatchItem"); ConnectorParameter itemparam = useItemDynamicPropertiesSpecialHandling ? patchItem.RequiredParameters[6] : patchItem.OptionalParameters[2]; + Assert.Equal(useItemDynamicPropertiesSpecialHandling, patchItem.SpecialBodyHandling); Assert.Equal(!useItemDynamicPropertiesSpecialHandling ? "dynamicProperties" : "item", itemparam.Name); FormulaValue[] parameters = new FormulaValue[7];