mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-16 10:58:30 -05:00
Insert sql bindings into Azure Functions (#1224)
* getting table name from a script * add InsertSqlInputBindingOperation * cleanup * move azure functions stuff out of dacfx service * cleanup * add tests * add another test * cleanup * add comments and connection string setting * addressing comments * change name to use add instead of insert
This commit is contained in:
@@ -26,6 +26,9 @@
|
|||||||
<PackageReference Update="Microsoft.SqlServer.Assessment" Version="[1.0.305]" />
|
<PackageReference Update="Microsoft.SqlServer.Assessment" Version="[1.0.305]" />
|
||||||
<PackageReference Update="Microsoft.SqlServer.Migration.Assessment" Version="1.0.20210714.5" />
|
<PackageReference Update="Microsoft.SqlServer.Migration.Assessment" Version="1.0.20210714.5" />
|
||||||
<PackageReference Update="Microsoft.Azure.OperationalInsights" Version="1.0.0" />
|
<PackageReference Update="Microsoft.Azure.OperationalInsights" Version="1.0.0" />
|
||||||
|
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
|
||||||
|
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
|
||||||
|
<PackageReference Update="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.10.0" />
|
||||||
|
|
||||||
<PackageReference Update="Moq" Version="4.8.2" />
|
<PackageReference Update="Moq" Version="4.8.2" />
|
||||||
<PackageReference Update="nunit" Version="3.12.0" />
|
<PackageReference Update="nunit" Version="3.12.0" />
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||||
|
using Microsoft.SqlTools.Utility;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
|
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class to represent inserting a sql binding into an Azure Function
|
||||||
|
/// </summary>
|
||||||
|
class AddSqlBindingOperation
|
||||||
|
{
|
||||||
|
const string functionAttributeText = "FunctionName";
|
||||||
|
|
||||||
|
public AddSqlBindingParams Parameters { get; }
|
||||||
|
|
||||||
|
public AddSqlBindingOperation(AddSqlBindingParams parameters)
|
||||||
|
{
|
||||||
|
Validate.IsNotNull("parameters", parameters);
|
||||||
|
this.Parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultStatus AddBinding()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = File.ReadAllText(Parameters.filePath);
|
||||||
|
|
||||||
|
SyntaxTree tree = CSharpSyntaxTree.ParseText(text);
|
||||||
|
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
|
||||||
|
|
||||||
|
// look for Azure Function to update
|
||||||
|
IEnumerable<MethodDeclarationSyntax> azureFunctionMethods = from methodDeclaration in root.DescendantNodes().OfType<MethodDeclarationSyntax>()
|
||||||
|
where methodDeclaration.AttributeLists.Count > 0
|
||||||
|
where methodDeclaration.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Contains(functionAttributeText) && attr.ArgumentList.Arguments.First().ToString().Equals($"\"{Parameters.functionName}\"")).Count() == 1).Count() == 1
|
||||||
|
select methodDeclaration;
|
||||||
|
|
||||||
|
if (azureFunctionMethods.Count() == 0)
|
||||||
|
{
|
||||||
|
return new ResultStatus()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = SR.CouldntFindAzureFunction(Parameters.functionName, Parameters.filePath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (azureFunctionMethods.Count() > 1)
|
||||||
|
{
|
||||||
|
return new ResultStatus()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = SR.MoreThanOneAzureFunctionWithName(Parameters.functionName, Parameters.filePath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclarationSyntax azureFunction = azureFunctionMethods.First();
|
||||||
|
var newParam = this.Parameters.bindingType == BindingType.input ? this.GenerateInputBinding() : this.GenerateOutputBinding();
|
||||||
|
|
||||||
|
// Generate updated method with the new parameter
|
||||||
|
// normalizewhitespace gets rid of any newline whitespace in the leading trivia, so we add that back
|
||||||
|
var updatedMethod = azureFunction.AddParameterListParameters(newParam).NormalizeWhitespace().WithLeadingTrivia(azureFunction.GetLeadingTrivia()).WithTrailingTrivia(azureFunction.GetTrailingTrivia());
|
||||||
|
|
||||||
|
// Replace the node in the tree
|
||||||
|
root = root.ReplaceNode(azureFunction, updatedMethod);
|
||||||
|
|
||||||
|
// write updated tree to file
|
||||||
|
var workspace = new AdhocWorkspace();
|
||||||
|
var syntaxTree = CSharpSyntaxTree.ParseText(root.ToString());
|
||||||
|
var formattedNode = CodeAnalysis.Formatting.Formatter.Format(syntaxTree.GetRoot(), workspace);
|
||||||
|
StringBuilder sb = new StringBuilder(formattedNode.ToString());
|
||||||
|
string content = sb.ToString();
|
||||||
|
File.WriteAllText(Parameters.filePath, content);
|
||||||
|
|
||||||
|
return new ResultStatus()
|
||||||
|
{
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return new ResultStatus()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = e.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a parameter for the sql input binding that looks like
|
||||||
|
/// [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result
|
||||||
|
/// </summary>
|
||||||
|
private ParameterSyntax GenerateInputBinding()
|
||||||
|
{
|
||||||
|
// Create arguments for the Sql Input Binding attribute
|
||||||
|
var argumentList = SyntaxFactory.AttributeArgumentList();
|
||||||
|
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"select * from {Parameters.objectName}\"")));
|
||||||
|
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("CommandType"), SyntaxFactory.IdentifierName("System.Data.CommandType.Text"))));
|
||||||
|
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
|
||||||
|
|
||||||
|
// Create Sql Binding attribute
|
||||||
|
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
|
||||||
|
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
|
||||||
|
|
||||||
|
// Create new parameter
|
||||||
|
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, new SyntaxTokenList(), SyntaxFactory.ParseTypeName("IEnumerable<Object>"), SyntaxFactory.Identifier("result"), null);
|
||||||
|
return newParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a parameter for the sql output binding that looks like
|
||||||
|
/// [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output
|
||||||
|
/// </summary>
|
||||||
|
private ParameterSyntax GenerateOutputBinding()
|
||||||
|
{
|
||||||
|
// Create arguments for the Sql Output Binding attribute
|
||||||
|
var argumentList = SyntaxFactory.AttributeArgumentList();
|
||||||
|
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{Parameters.objectName}\"")));
|
||||||
|
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
|
||||||
|
|
||||||
|
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
|
||||||
|
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
|
||||||
|
|
||||||
|
var syntaxTokenList = new SyntaxTokenList();
|
||||||
|
syntaxTokenList = syntaxTokenList.Add(SyntaxFactory.Token(SyntaxKind.OutKeyword));
|
||||||
|
|
||||||
|
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, syntaxTokenList, SyntaxFactory.ParseTypeName(typeof(Object).Name), SyntaxFactory.Identifier("output"), null);
|
||||||
|
return newParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.SqlTools.Hosting.Protocol;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||||
|
|
||||||
|
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Main class for Azure Functions service
|
||||||
|
/// </summary>
|
||||||
|
class AzureFunctionsService
|
||||||
|
{
|
||||||
|
private static readonly Lazy<AzureFunctionsService> instance = new Lazy<AzureFunctionsService>(() => new AzureFunctionsService());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the singleton instance object
|
||||||
|
/// </summary>
|
||||||
|
public static AzureFunctionsService Instance
|
||||||
|
{
|
||||||
|
get { return instance.Value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the service instance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceHost"></param>
|
||||||
|
public void InitializeService(ServiceHost serviceHost)
|
||||||
|
{
|
||||||
|
serviceHost.SetRequestHandler(AddSqlBindingRequest.Type, this.HandleAddSqlBindingRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles request to add sql binding into Azure Functions
|
||||||
|
/// </summary>
|
||||||
|
public async Task HandleAddSqlBindingRequest(AddSqlBindingParams parameters, RequestContext<ResultStatus> requestContext)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
|
||||||
|
ResultStatus result = operation.AddBinding();
|
||||||
|
|
||||||
|
await requestContext.SendResult(result);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await requestContext.SendError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||||
|
|
||||||
|
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Binding types for sql bindings for Azure Functions
|
||||||
|
/// </summary>
|
||||||
|
public enum BindingType
|
||||||
|
{
|
||||||
|
input,
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters for adding a sql binding
|
||||||
|
/// </summary>
|
||||||
|
public class AddSqlBindingParams
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the filePath
|
||||||
|
/// </summary>
|
||||||
|
public string filePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the binding type
|
||||||
|
/// </summary>
|
||||||
|
public BindingType bindingType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the function name
|
||||||
|
/// </summary>
|
||||||
|
public string functionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the object name
|
||||||
|
/// </summary>
|
||||||
|
public string objectName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the connection string setting
|
||||||
|
/// </summary>
|
||||||
|
public string connectionStringSetting { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the Add Sql Binding request
|
||||||
|
/// </summary>
|
||||||
|
class AddSqlBindingRequest
|
||||||
|
{
|
||||||
|
public static readonly RequestType<AddSqlBindingParams, ResultStatus> Type =
|
||||||
|
RequestType<AddSqlBindingParams, ResultStatus>.Create("azureFunctions/sqlBinding");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ using Microsoft.SqlTools.Hosting;
|
|||||||
using Microsoft.SqlTools.Hosting.Protocol;
|
using Microsoft.SqlTools.Hosting.Protocol;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Admin;
|
using Microsoft.SqlTools.ServiceLayer.Admin;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Agent;
|
using Microsoft.SqlTools.ServiceLayer.Agent;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AzureFunctions;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Cms;
|
using Microsoft.SqlTools.ServiceLayer.Cms;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||||
using Microsoft.SqlTools.ServiceLayer.DacFx;
|
using Microsoft.SqlTools.ServiceLayer.DacFx;
|
||||||
@@ -133,6 +134,9 @@ namespace Microsoft.SqlTools.ServiceLayer
|
|||||||
SchemaCompare.SchemaCompareService.Instance.InitializeService(serviceHost);
|
SchemaCompare.SchemaCompareService.Instance.InitializeService(serviceHost);
|
||||||
serviceProvider.RegisterSingleService(SchemaCompareService.Instance);
|
serviceProvider.RegisterSingleService(SchemaCompareService.Instance);
|
||||||
|
|
||||||
|
AzureFunctions.AzureFunctionsService.Instance.InitializeService(serviceHost);
|
||||||
|
serviceProvider.RegisterSingleService(AzureFunctionsService.Instance);
|
||||||
|
|
||||||
ServerConfigService.Instance.InitializeService(serviceHost);
|
ServerConfigService.Instance.InitializeService(serviceHost);
|
||||||
serviceProvider.RegisterSingleService(ServerConfigService.Instance);
|
serviceProvider.RegisterSingleService(ServerConfigService.Instance);
|
||||||
|
|
||||||
|
|||||||
@@ -3278,6 +3278,16 @@ namespace Microsoft.SqlTools.ServiceLayer
|
|||||||
return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode);
|
return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string CouldntFindAzureFunction(string functionName, string fileName)
|
||||||
|
{
|
||||||
|
return Keys.GetString(Keys.CouldntFindAzureFunction, functionName, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MoreThanOneAzureFunctionWithName(string functionName, string fileName)
|
||||||
|
{
|
||||||
|
return Keys.GetString(Keys.MoreThanOneAzureFunctionWithName, functionName, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
public class Keys
|
public class Keys
|
||||||
{
|
{
|
||||||
@@ -4555,6 +4565,12 @@ namespace Microsoft.SqlTools.ServiceLayer
|
|||||||
public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition";
|
public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition";
|
||||||
|
|
||||||
|
|
||||||
|
public const string CouldntFindAzureFunction = "CouldntFindAzureFunction";
|
||||||
|
|
||||||
|
|
||||||
|
public const string MoreThanOneAzureFunctionWithName = "MoreThanOneAzureFunctionWithName";
|
||||||
|
|
||||||
|
|
||||||
private Keys()
|
private Keys()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|||||||
@@ -1854,4 +1854,14 @@
|
|||||||
<comment>.
|
<comment>.
|
||||||
Parameters: 0 - editionCode (int) </comment>
|
Parameters: 0 - editionCode (int) </comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CouldntFindAzureFunction" xml:space="preserve">
|
||||||
|
<value>Couldn't find Azure function with FunctionName {0} in {1}</value>
|
||||||
|
<comment>.
|
||||||
|
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
|
||||||
|
</data>
|
||||||
|
<data name="MoreThanOneAzureFunctionWithName" xml:space="preserve">
|
||||||
|
<value>More than one Azure function found with the FunctionName {0} in {1}</value>
|
||||||
|
<comment>.
|
||||||
|
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -848,4 +848,9 @@ SchemaCompareSessionNotFound = Could not find the schema compare session to canc
|
|||||||
SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script
|
SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script
|
||||||
SqlAssessmentQueryInvalidOwnerUri = Not connected to a server
|
SqlAssessmentQueryInvalidOwnerUri = Not connected to a server
|
||||||
SqlAssessmentConnectingError = Cannot connect to the server
|
SqlAssessmentConnectingError = Cannot connect to the server
|
||||||
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
|
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
# Azure Functions
|
||||||
|
CouldntFindAzureFunction(string functionName, string fileName) = Couldn't find Azure function with FunctionName '{0}' in {1}
|
||||||
|
MoreThanOneAzureFunctionWithName(string functionName, string fileName) = More than one Azure function found with the FunctionName '{0}' in {1}
|
||||||
@@ -2161,6 +2161,16 @@
|
|||||||
<target state="new">No External Streaming Job creation TSQL found (EXEC sp_create_streaming_job statement).</target>
|
<target state="new">No External Streaming Job creation TSQL found (EXEC sp_create_streaming_job statement).</target>
|
||||||
<note></note>
|
<note></note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="CouldntFindAzureFunction">
|
||||||
|
<source>Couldn't find Azure function with FunctionName {0} in {1}</source>
|
||||||
|
<target state="new">Couldn't find Azure function with FunctionName {0} in {1}</target>
|
||||||
|
<note></note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="MoreThanOneAzureFunctionWithName">
|
||||||
|
<source>More than one Azure function found with the FunctionName {0} in {1}</source>
|
||||||
|
<target state="new">More than one Azure function found with the FunctionName {0} in {1}</target>
|
||||||
|
<note></note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
|
||||||
<PackageReference Include="Microsoft.SqlServer.DACFx" />
|
<PackageReference Include="Microsoft.SqlServer.DACFx" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" />
|
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Company.Namespace.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Azure.WebJobs;
|
||||||
|
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Company.Namespace
|
||||||
|
{
|
||||||
|
public class ArtistsApi
|
||||||
|
{
|
||||||
|
private ILogger<ArtistsApi> _logger;
|
||||||
|
|
||||||
|
/// <summary> Initializes a new instance of ArtistsApi. </summary>
|
||||||
|
/// <param name="logger"> Class logger. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
|
||||||
|
public ArtistsApi(ILogger<ArtistsApi> logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Returns a list of artists. </summary>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
[FunctionName("GetArtists_get")]
|
||||||
|
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Lets a user post a new artist. </summary>
|
||||||
|
/// <param name="body"> The Artist to use. </param>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
|
||||||
|
[FunctionName("NewArtist_post")]
|
||||||
|
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Company.Namespace.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Azure.WebJobs;
|
||||||
|
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Company.Namespace
|
||||||
|
{
|
||||||
|
public class ArtistsApi
|
||||||
|
{
|
||||||
|
private ILogger<ArtistsApi> _logger;
|
||||||
|
|
||||||
|
/// <summary> Initializes a new instance of ArtistsApi. </summary>
|
||||||
|
/// <param name="logger"> Class logger. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
|
||||||
|
public ArtistsApi(ILogger<ArtistsApi> logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Returns a list of artists. </summary>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
[FunctionName("GetArtists_get")]
|
||||||
|
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Lets a user post a new artist. </summary>
|
||||||
|
/// <param name="body"> The Artist to use. </param>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
|
||||||
|
[FunctionName("GetArtists_get")]
|
||||||
|
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Company.Namespace.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Azure.WebJobs;
|
||||||
|
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Company.Namespace
|
||||||
|
{
|
||||||
|
public class ArtistsApi
|
||||||
|
{
|
||||||
|
private ILogger<ArtistsApi> _logger;
|
||||||
|
|
||||||
|
/// <summary> Initializes a new instance of ArtistsApi. </summary>
|
||||||
|
/// <param name="logger"> Class logger. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
|
||||||
|
public ArtistsApi(ILogger<ArtistsApi> logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Returns a list of artists. </summary>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
[FunctionName("GetArtists_get")]
|
||||||
|
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Lets a user post a new artist. </summary>
|
||||||
|
/// <param name="body"> The Artist to use. </param>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
|
||||||
|
[FunctionName("NewArtist_post")]
|
||||||
|
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Company.Namespace.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Azure.WebJobs;
|
||||||
|
using Microsoft.Azure.WebJobs.Extensions.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Company.Namespace
|
||||||
|
{
|
||||||
|
public class ArtistsApi
|
||||||
|
{
|
||||||
|
private ILogger<ArtistsApi> _logger;
|
||||||
|
|
||||||
|
/// <summary> Initializes a new instance of ArtistsApi. </summary>
|
||||||
|
/// <param name="logger"> Class logger. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="logger"/> is null. </exception>
|
||||||
|
public ArtistsApi(ILogger<ArtistsApi> logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Returns a list of artists. </summary>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
[FunctionName("GetArtists_get")]
|
||||||
|
public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Lets a user post a new artist. </summary>
|
||||||
|
/// <param name="body"> The Artist to use. </param>
|
||||||
|
/// <param name="req"> Raw HTTP Request. </param>
|
||||||
|
/// <param name="cancellationToken"> The cancellation token provided on Function shutdown. </param>
|
||||||
|
/// <exception cref="ArgumentNullException"> <paramref name="body"/> is null. </exception>
|
||||||
|
[FunctionName("NewArtist_post")]
|
||||||
|
public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req, [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HTTP trigger function processed a request.");
|
||||||
|
// TODO: Handle Documented Responses.
|
||||||
|
// Spec Defines: HTTP 200
|
||||||
|
// Spec Defines: HTTP 400
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using Microsoft.SqlTools.Hosting.Protocol;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AzureFunctions;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions
|
||||||
|
{
|
||||||
|
class AzureFunctionsServiceTests
|
||||||
|
{
|
||||||
|
private string testAzureFunctionsFolder = Path.Combine("..", "..", "..", "AzureFunctions", "AzureFunctionTestFiles");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify input binding gets added
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void AddSqlInputBinding()
|
||||||
|
{
|
||||||
|
// copy the original file because the input binding will be inserted into the file
|
||||||
|
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
|
||||||
|
string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlInputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
|
||||||
|
File.Copy(originalFile, testFile, true);
|
||||||
|
|
||||||
|
AddSqlBindingParams parameters = new AddSqlBindingParams
|
||||||
|
{
|
||||||
|
bindingType = BindingType.input,
|
||||||
|
filePath = testFile,
|
||||||
|
functionName = "GetArtists_get",
|
||||||
|
objectName = "[dbo].[table1]",
|
||||||
|
connectionStringSetting = "SqlConnectionString"
|
||||||
|
};
|
||||||
|
|
||||||
|
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
|
||||||
|
ResultStatus result = operation.AddBinding();
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.IsNull(result.ErrorMessage);
|
||||||
|
|
||||||
|
string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsInputBinding.ts"));
|
||||||
|
string actualFileText = File.ReadAllText(testFile);
|
||||||
|
Assert.AreEqual(expectedFileText, actualFileText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify output binding gets added
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void AddSqlOutputBinding()
|
||||||
|
{
|
||||||
|
// copy the original file because the output binding will be inserted into the file
|
||||||
|
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
|
||||||
|
string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlOutputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
|
||||||
|
File.Copy(originalFile, testFile, true);
|
||||||
|
|
||||||
|
AddSqlBindingParams parameters = new AddSqlBindingParams
|
||||||
|
{
|
||||||
|
bindingType = BindingType.output,
|
||||||
|
filePath = testFile,
|
||||||
|
functionName = "NewArtist_post",
|
||||||
|
objectName = "[dbo].[table1]",
|
||||||
|
connectionStringSetting = "SqlConnectionString"
|
||||||
|
};
|
||||||
|
|
||||||
|
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
|
||||||
|
ResultStatus result = operation.AddBinding();
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.IsNull(result.ErrorMessage);
|
||||||
|
|
||||||
|
string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsOutputBinding.ts"));
|
||||||
|
string actualFileText = File.ReadAllText(testFile);
|
||||||
|
Assert.AreEqual(expectedFileText, actualFileText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify what happens when specified azure function isn't found
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void NoAzureFunctionForSqlBinding()
|
||||||
|
{
|
||||||
|
// copy the original file because the input binding will be inserted into the file
|
||||||
|
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts");
|
||||||
|
string testFile = Path.Join(Path.GetTempPath(), string.Format("NoAzureFunctionForSqlBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
|
||||||
|
File.Copy(originalFile, testFile, true);
|
||||||
|
|
||||||
|
AddSqlBindingParams parameters = new AddSqlBindingParams
|
||||||
|
{
|
||||||
|
bindingType = BindingType.input,
|
||||||
|
filePath = testFile,
|
||||||
|
functionName = "noExistingFunction",
|
||||||
|
objectName = "[dbo].[table1]",
|
||||||
|
connectionStringSetting = "SqlConnectionString"
|
||||||
|
};
|
||||||
|
|
||||||
|
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
|
||||||
|
ResultStatus result = operation.AddBinding();
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.NotNull(result.ErrorMessage);
|
||||||
|
Assert.True(result.ErrorMessage.Equals(SR.CouldntFindAzureFunction("noExistingFunction", testFile)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify what happens when there's more than one Azure function with the specified name in the file
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void MoreThanOneAzureFunctionWithSpecifiedName()
|
||||||
|
{
|
||||||
|
// copy the original file because the input binding will be inserted into the file
|
||||||
|
string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsMultipleSameFunction.ts");
|
||||||
|
string testFile = Path.Join(Path.GetTempPath(), string.Format("MoreThanOneAzureFunctionWithSpecifiedName-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")));
|
||||||
|
File.Copy(originalFile, testFile, true);
|
||||||
|
|
||||||
|
AddSqlBindingParams parameters = new AddSqlBindingParams
|
||||||
|
{
|
||||||
|
bindingType = BindingType.input,
|
||||||
|
filePath = testFile,
|
||||||
|
functionName = "GetArtists_get",
|
||||||
|
objectName = "[dbo].[table1]",
|
||||||
|
connectionStringSetting = "SqlConnectionString"
|
||||||
|
};
|
||||||
|
|
||||||
|
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
|
||||||
|
ResultStatus result = operation.AddBinding();
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.NotNull(result.ErrorMessage);
|
||||||
|
Assert.True(result.ErrorMessage.Equals(SR.MoreThanOneAzureFunctionWithName("GetArtists_get", testFile)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user