diff --git a/ParameterizationExtractor.WebApi/Controllers/ExtractionController.cs b/ParameterizationExtractor.WebApi/Controllers/ExtractionController.cs new file mode 100644 index 0000000..b42693d --- /dev/null +++ b/ParameterizationExtractor.WebApi/Controllers/ExtractionController.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Mvc; +using Quipu.ParameterizationExtractor.WebApi.Models; +using Quipu.ParameterizationExtractor.WebApi.Services; + +namespace Quipu.ParameterizationExtractor.WebApi.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ExtractionController : ControllerBase + { + private readonly IExtractionService _extractionService; + private readonly ILogger _logger; + + public ExtractionController(IExtractionService extractionService, ILogger logger) + { + _extractionService = extractionService; + _logger = logger; + } + + /// + /// + [HttpPost("extract")] + public async Task ExtractAsync([FromBody] ExtractionRequest request, CancellationToken cancellationToken = default) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + _logger.LogInformation("Received extraction request for config type: {ConfigType}", request.ConfigType); + + var result = await _extractionService.ProcessExtractionAsync(request, cancellationToken); + + if (!result.Success) + { + return BadRequest(new { error = result.ErrorMessage }); + } + + return File(result.ZipFileContent!, "application/zip", result.FileName); + } + + /// + /// + [HttpGet("health")] + public IActionResult Health() + { + return Ok(new { status = "healthy", timestamp = DateTime.UtcNow }); + } + + /// + /// + [HttpGet("formats")] + public IActionResult GetFormats() + { + var formats = new + { + supported_formats = new[] { "DSL", "JSON", "XML" }, + examples = new + { + dsl = @"for script ""example"" take from ""Users"" where ""Active = 1"" consider FK for ""Users"" and UniqueColumns ""Id"" build sql with asIs", + json = new + { + scripts = new[] + { + new + { + scriptName = "example", + rootRecords = new[] + { + new { tableName = "Users", where = "Active = 1", processingOrder = 0 } + }, + tablesToProcess = new[] + { + new + { + tableName = "Users", + uniqueColumns = new[] { "Id" }, + extractStrategy = "FK", + sqlBuildOptions = new[] { "AsIsInserts" } + } + } + } + } + }, + xml = @"Id" + } + }; + + return Ok(formats); + } + } +} diff --git a/ParameterizationExtractor.WebApi/Models/ApiAppArgs.cs b/ParameterizationExtractor.WebApi/Models/ApiAppArgs.cs new file mode 100644 index 0000000..c028628 --- /dev/null +++ b/ParameterizationExtractor.WebApi/Models/ApiAppArgs.cs @@ -0,0 +1,24 @@ +using Quipu.ParameterizationExtractor.Common; + +namespace Quipu.ParameterizationExtractor.WebApi.Models +{ + public class ApiAppArgs : IAppArgs + { + public string DBName { get; set; } = string.Empty; + public string ServerName { get; set; } = string.Empty; + public string PathToPackage { get; set; } = string.Empty; + public string ConnectionName { get; set; } = "DefaultConnection"; + public string OutputFolder { get; set; } = string.Empty; + public bool Interactive { get; set; } = false; + + public static ApiAppArgs Create(string outputFolder, string connectionName = "DefaultConnection") + { + return new ApiAppArgs + { + OutputFolder = outputFolder, + ConnectionName = connectionName, + Interactive = false + }; + } + } +} diff --git a/ParameterizationExtractor.WebApi/Models/ExtractionRequest.cs b/ParameterizationExtractor.WebApi/Models/ExtractionRequest.cs new file mode 100644 index 0000000..7fcddb2 --- /dev/null +++ b/ParameterizationExtractor.WebApi/Models/ExtractionRequest.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace Quipu.ParameterizationExtractor.WebApi.Models +{ + public class ExtractionRequest + { + [Required] + public string ConnectionString { get; set; } = string.Empty; + + [Required] + public string Configuration { get; set; } = string.Empty; + + [Required] + public ConfigurationType ConfigType { get; set; } + + public string? OutputFileName { get; set; } + } + + public enum ConfigurationType + { + DSL, + JSON, + XML + } + + public class ExtractionResponse + { + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + public byte[]? ZipFileContent { get; set; } + public string? FileName { get; set; } + } +} diff --git a/ParameterizationExtractor.WebApi/ParameterizationExtractor.WebApi.csproj b/ParameterizationExtractor.WebApi/ParameterizationExtractor.WebApi.csproj new file mode 100644 index 0000000..7407d0a --- /dev/null +++ b/ParameterizationExtractor.WebApi/ParameterizationExtractor.WebApi.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + Quipu.ParameterizationExtractor.WebApi + + + + + + + + + + + + + + + + + + diff --git a/ParameterizationExtractor.WebApi/Program.cs b/ParameterizationExtractor.WebApi/Program.cs new file mode 100644 index 0000000..aa41eb2 --- /dev/null +++ b/ParameterizationExtractor.WebApi/Program.cs @@ -0,0 +1,71 @@ +using Microsoft.OpenApi.Models; +using Quipu.ParameterizationExtractor.WebApi.Services; +using Quipu.ParameterizationExtractor.WebApi.Models; +using Quipu.ParameterizationExtractor.Logic.Interfaces; +using Quipu.ParameterizationExtractor.Logic.MSSQL; +using Quipu.ParameterizationExtractor.DSL.Connector; +using Quipu.ParameterizationExtractor; +using Quipu.ParameterizationExtractor.Common; +using Quipu.ParameterizationExtractor.Configs; +using ParameterizationExtractor.Logic.MSSQL; +using Quipu.ParameterizationExtractor.Logic.Configs; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "ParameterizationExtractor API", + Version = "v1", + Description = "REST API for database extraction and SQL script generation" + }); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(provider => new ApiAppArgs()); + +builder.Services.AddSingleton(provider => +{ + var dslConnector = provider.GetRequiredService(); + var configSerializer = new ConfigSerializer(dslConnector); + try + { + return configSerializer.GetGlobalConfig(); + } + catch + { + return new GlobalExtractConfiguration(); + } +}); + +builder.Services.AddLogging(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/ParameterizationExtractor.WebApi/Services/ExtractionService.cs b/ParameterizationExtractor.WebApi/Services/ExtractionService.cs new file mode 100644 index 0000000..0e148d9 --- /dev/null +++ b/ParameterizationExtractor.WebApi/Services/ExtractionService.cs @@ -0,0 +1,240 @@ +using System.IO.Compression; +using System.Text; +using Microsoft.Extensions.Logging; +using Quipu.ParameterizationExtractor.WebApi.Models; +using Quipu.ParameterizationExtractor.Logic.Interfaces; +using Quipu.ParameterizationExtractor.DSL.Connector; +using Quipu.ParameterizationExtractor.Logic.Configs; +using Quipu.ParameterizationExtractor.Configs; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; + +namespace Quipu.ParameterizationExtractor.WebApi.Services +{ + public class ExtractionService : IExtractionService + { + private readonly ILogger _logger; + private readonly IDSLConnector _dslConnector; + private readonly PackageProcessor _packageProcessor; + private readonly IServiceProvider _serviceProvider; + + public ExtractionService( + ILogger logger, + IDSLConnector dslConnector, + PackageProcessor packageProcessor, + IServiceProvider serviceProvider) + { + _logger = logger; + _dslConnector = dslConnector; + _packageProcessor = packageProcessor; + _serviceProvider = serviceProvider; + } + + public async Task ProcessExtractionAsync(ExtractionRequest request, CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Processing extraction request with config type: {ConfigType}", request.ConfigType); + + IPackage package = request.ConfigType switch + { + ConfigurationType.DSL => ParseDSLConfiguration(request.Configuration), + ConfigurationType.JSON => ParseJSONConfiguration(request.Configuration), + ConfigurationType.XML => ParseXMLConfiguration(request.Configuration), + _ => throw new ArgumentException($"Unsupported configuration type: {request.ConfigType}") + }; + + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + using var scope = _serviceProvider.CreateScope(); + var scopedAppArgs = scope.ServiceProvider.GetRequiredService(); + if (scopedAppArgs is ApiAppArgs apiArgs) + { + apiArgs.OutputFolder = tempDir; + apiArgs.ConnectionName = "DefaultConnection"; + } + + Environment.SetEnvironmentVariable("ConnectionStrings__DefaultConnection", request.ConnectionString); + + var scopedPackageProcessor = scope.ServiceProvider.GetRequiredService(); + await scopedPackageProcessor.ExecuteAsync(cancellationToken, package); + + var zipContent = await CreateZipFromDirectoryAsync(tempDir); + var fileName = request.OutputFileName ?? $"extraction_{DateTime.UtcNow:yyyyMMdd_HHmmss}.zip"; + + return new ExtractionResponse + { + Success = true, + ZipFileContent = zipContent, + FileName = fileName + }; + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + catch (DSLParseException ex) + { + _logger.LogError(ex, "DSL parsing error"); + return new ExtractionResponse + { + Success = false, + ErrorMessage = $"DSL parsing error: {ex.Message}" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing extraction request"); + return new ExtractionResponse + { + Success = false, + ErrorMessage = $"Processing error: {ex.Message}" + }; + } + } + + private IPackage ParseDSLConfiguration(string dslContent) + { + return _dslConnector.Parse(dslContent); + } + + private IPackage ParseJSONConfiguration(string jsonContent) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var packageData = JsonSerializer.Deserialize(jsonContent, options); + return ConvertToPackage(packageData); + } + + private IPackage ParseXMLConfiguration(string xmlContent) + { + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, xmlContent); + var configSerializer = new ConfigSerializer(_dslConnector); + return configSerializer.GetPackage(tempFile); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + private IPackage ConvertToPackage(PackageData packageData) + { + var package = new Package(); + + foreach (var scriptData in packageData.Scripts) + { + var script = new SourceForScript + { + ScriptName = scriptData.ScriptName + }; + + foreach (var rootRecord in scriptData.RootRecords) + { + script.RootRecords.Add(new RecordsToExtract + { + TableName = rootRecord.TableName, + Where = rootRecord.Where, + ProcessingOrder = rootRecord.ProcessingOrder + }); + } + + foreach (var table in scriptData.TablesToProcess) + { + var tableToExtract = new TableToExtract + { + TableName = table.TableName, + UniqueColumns = table.UniqueColumns?.ToList() ?? new List() + }; + + tableToExtract.ExtractStrategy = table.ExtractStrategy?.ToLower() switch + { + "onlyonetableextract" or "onetable" => new OnlyOneTableExtractStrategy(), + "fkdependencyextract" or "fk" => new FKDependencyExtractStrategy(), + "parentsextract" or "parents" => new OnlyParentExtractStrategy(), + "childrenextract" or "children" => new OnlyChildrenExtractStrategy(), + _ => new FKDependencyExtractStrategy() + }; + + tableToExtract.SqlBuildStrategy = new SqlBuildStrategy + { + AsIsInserts = table.SqlBuildOptions?.Contains("AsIsInserts") ?? false, + NoInserts = table.SqlBuildOptions?.Contains("NoInserts") ?? false, + ThrowExecptionIfNotExists = table.SqlBuildOptions?.Contains("ThrowExceptionIfNotExists") ?? false + }; + + script.TablesToProcess.Add(tableToExtract); + } + + package.Scripts.Add(script); + } + + return package; + } + + private async Task CreateZipFromDirectoryAsync(string directoryPath) + { + using var memoryStream = new MemoryStream(); + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); + + foreach (var file in files) + { + var relativePath = Path.GetRelativePath(directoryPath, file); + var entry = archive.CreateEntry(relativePath); + + using var entryStream = entry.Open(); + using var fileStream = File.OpenRead(file); + await fileStream.CopyToAsync(entryStream); + } + } + + return memoryStream.ToArray(); + } + } + + public class PackageData + { + public List Scripts { get; set; } = new(); + } + + public class ScriptData + { + public string ScriptName { get; set; } = string.Empty; + public List RootRecords { get; set; } = new(); + public List TablesToProcess { get; set; } = new(); + } + + public class RootRecordData + { + public string TableName { get; set; } = string.Empty; + public string Where { get; set; } = string.Empty; + public int ProcessingOrder { get; set; } + } + + public class TableData + { + public string TableName { get; set; } = string.Empty; + public string[]? UniqueColumns { get; set; } + public string? ExtractStrategy { get; set; } + public string[]? SqlBuildOptions { get; set; } + public string[]? Exclude { get; set; } + } +} diff --git a/ParameterizationExtractor.WebApi/Services/IExtractionService.cs b/ParameterizationExtractor.WebApi/Services/IExtractionService.cs new file mode 100644 index 0000000..6a40965 --- /dev/null +++ b/ParameterizationExtractor.WebApi/Services/IExtractionService.cs @@ -0,0 +1,9 @@ +using Quipu.ParameterizationExtractor.WebApi.Models; + +namespace Quipu.ParameterizationExtractor.WebApi.Services +{ + public interface IExtractionService + { + Task ProcessExtractionAsync(ExtractionRequest request, CancellationToken cancellationToken = default); + } +} diff --git a/ParameterizationExtractor.WebApi/appsettings.Development.json b/ParameterizationExtractor.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ParameterizationExtractor.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ParameterizationExtractor.WebApi/appsettings.json b/ParameterizationExtractor.WebApi/appsettings.json new file mode 100644 index 0000000..a298580 --- /dev/null +++ b/ParameterizationExtractor.WebApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=YourDatabase;Trusted_Connection=true;" + } +} diff --git a/SQL Buldozer.sln b/SQL Buldozer.sln index d048a2e..0e5afc4 100644 --- a/SQL Buldozer.sln +++ b/SQL Buldozer.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParameterizationExtractor", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{44C4463E-743D-41A6-9AC2-DB8E2758CE46}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParameterizationExtractor.WebApi", "ParameterizationExtractor.WebApi\ParameterizationExtractor.WebApi.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {44C4463E-743D-41A6-9AC2-DB8E2758CE46}.Debug|Any CPU.Build.0 = Debug|Any CPU {44C4463E-743D-41A6-9AC2-DB8E2758CE46}.Release|Any CPU.ActiveCfg = Release|Any CPU {44C4463E-743D-41A6-9AC2-DB8E2758CE46}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE