Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ServiceLayer.Mesh/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class AppConfiguration :
IFileExtractFunctionConfiguration,
IFileExtractQueueClientConfiguration,
IFileTransformQueueClientConfiguration,
IFileTransformFunctionConfiguration,
IFileRetryFunctionConfiguration,
IMeshHandshakeFunctionConfiguration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileTransformFunctionConfiguration
{
int StaleHours { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

public class FileParser : IFileParser
{
public ParsedFile Parse(Stream stream)
{
// TODO - implement this
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
using ServiceLayer.Mesh.Models;

Check failure on line 2 in src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'Models' does not exist in the namespace 'ServiceLayer.Mesh' (are you missing an assembly reference?)

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - NBSS appointment file specific implementation of IFileTransformer. To orchestrate parsing, validation and staging of data (delegated to separate classes)
public class FileTransformer : IFileTransformer
{
private readonly IFileParser _fileParser;
private readonly IValidationRunner _validationRunner;
private readonly IStagingPersister _stagingPersister;

public FileTransformer(IFileParser fileParser, IValidationRunner validationRunner, IStagingPersister stagingPersister)
{
_fileParser = fileParser;
_validationRunner = validationRunner;
_stagingPersister = stagingPersister;
}

public MeshFileType HandlesFileType => MeshFileType.NbssAppointmentEvents;

Check failure on line 20 in src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'MeshFileType' could not be found (are you missing a using directive or an assembly reference?)

public async Task<IList<ValidationError>> TransformFileAsync(Stream stream, MeshFile metaData)

Check failure on line 22 in src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'MeshFile' could not be found (are you missing a using directive or an assembly reference?)
{
// TODO - consider whether we should wrap this parsing in a try-catch and return a List<ValidationError> in case of any unforeseen parsing issues (file is totally unlike anything we expect)
var parsed = _fileParser.Parse(stream);

var validationErrors = _validationRunner.Validate(parsed);
if (!validationErrors.Any())
{
await _stagingPersister.WriteStagedData(parsed);
}

return validationErrors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

public interface IFileParser
{
ParsedFile Parse(Stream stream);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - interface for class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table
public interface IStagingPersister
{
Task WriteStagedData(ParsedFile parsedFile);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class FileControlRecord
{
public string? RecordTypeIdentifier { get; set; }

public string? ExtractId { get; set; }

public string? TransferStartDate { get; set; }

public string? TransferStartTime { get; set; }

public string? RecordCount { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class FileDataRecord
{
public int RowNumber { get; set; }
public Dictionary<string, string> Fields { get; } = [];

public string? this[string fieldName] => Fields.GetValueOrDefault(fieldName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class ParsedFile
{
public FileControlRecord? FileHeader { get; set; }
public FileControlRecord? FileTrailer { get; set; }
public required List<string> ColumnHeadings { get; set; } = [];
public required List<FileDataRecord> DataRecords { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table
public class StagingPersister : IStagingPersister
{
public Task WriteStagedData(ParsedFile parsedFile)
{
// TODO - implement this
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

// TODO - create a whole bunch of implementations of this to perform the validation against NBSS Appointment events files
public interface IFileValidator
{
IEnumerable<ValidationError> Validate(ParsedFile file);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

// TODO - create a whole bunch of implementations of this to perform the validation against NBSS Appointment events records
public interface IRecordValidator
{
IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public interface IValidationRunner
{
IList<ValidationError> Validate(ParsedFile file);
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public class ValidationRunner(
IEnumerable<IFileValidator> fileValidators,
IEnumerable<IRecordValidator> recordValidators)
: IValidationRunner
{
public IList<ValidationError> Validate(ParsedFile file)
{
var errors = new List<ValidationError>();

foreach (var dataRecord in file.DataRecords)
{
foreach (var recordValidator in recordValidators)
{
errors.AddRange(recordValidator.Validate(dataRecord));
}
}

foreach (var validator in fileValidators)
{
errors.AddRange(validator.Validate(file));
}

return errors;
}
}
66 changes: 65 additions & 1 deletion src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
using Google.Protobuf.WellKnownTypes;
using Microsoft.Azure.Functions.Worker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ServiceLayer.Mesh.Configuration;
using ServiceLayer.Mesh.Data;

Check failure on line 6 in src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'Data' does not exist in the namespace 'ServiceLayer.Mesh' (are you missing an assembly reference?)
using ServiceLayer.Mesh.Messaging;
using ServiceLayer.Mesh.Models;

Check failure on line 8 in src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'Models' does not exist in the namespace 'ServiceLayer.Mesh' (are you missing an assembly reference?)
using ServiceLayer.Mesh.Storage;

namespace ServiceLayer.Mesh.Functions;

public class FileTransformFunction
public class FileTransformFunction(
ILogger<FileTransformFunction> logger,
ServiceLayerDbContext serviceLayerDbContext,

Check failure on line 15 in src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'ServiceLayerDbContext' could not be found (are you missing a using directive or an assembly reference?)
IMeshFilesBlobStore meshFileBlobStore,
IFileTransformFunctionConfiguration configuration)
{
[Function("FileTransformFunction")]
public async Task Run([QueueTrigger("%FileTransformQueueName%")] FileTransformQueueMessage message)
{
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();

var file = await serviceLayerDbContext.MeshFiles.FirstOrDefaultAsync(f => f.FileId == message.FileId);

if (file == null)
{
logger.LogWarning("File with id: {fileId} not found in MeshFiles table.", message.FileId);
return;
}

if (!IsFileSuitableForTransformation(file))
{
return;
}

await UpdateFileStatusForTransformation(file);
await transaction.CommitAsync();

var fileContent = await meshFileBlobStore.DownloadAsync(file);

// TODO - take dependency on IEnumerable<IFileTransformer>.
// After initial common checks against database, find the appropriate implementation of IFileTransformer to handle the functionality that differs between file type.
}

private async Task UpdateFileStatusForTransformation(MeshFile file)
{
file.Status = MeshFileStatus.Transforming;
file.LastUpdatedUtc = DateTime.UtcNow;
await serviceLayerDbContext.SaveChangesAsync();
}

private bool IsFileSuitableForTransformation(MeshFile file)

Check failure on line 53 in src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'MeshFile' could not be found (are you missing a using directive or an assembly reference?)
{
// We only want to transform files if they are in a Extracted state,
// or are in a Transforming state and were last touched over 12 hours ago.
var expectedStatuses = new[] { MeshFileStatus.Extracted, MeshFileStatus.Transforming };
if (!expectedStatuses.Contains(file.Status) ||
(file.Status == MeshFileStatus.Transforming && file.LastUpdatedUtc > DateTime.UtcNow.AddHours(-configuration.StaleHours)))
{
logger.LogWarning(
"File with id: {fileId} found in MeshFiles table but is not suitable for transformation. Status: {status}, LastUpdatedUtc: {lastUpdatedUtc}.",
file.FileId,
file.Status,
file.LastUpdatedUtc.ToTimestamp());
return false;
}
return true;
}
}
9 changes: 9 additions & 0 deletions src/ServiceLayer.Mesh/IFileTransformer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.Models;

Check failure on line 1 in src/ServiceLayer.Mesh/IFileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'Models' does not exist in the namespace 'ServiceLayer.Mesh' (are you missing an assembly reference?)

namespace ServiceLayer.Mesh;

public interface IFileTransformer
{
MeshFileType HandlesFileType { get; }

Check failure on line 7 in src/ServiceLayer.Mesh/IFileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'MeshFileType' could not be found (are you missing a using directive or an assembly reference?)
Task<IList<ValidationError>> TransformFileAsync(Stream stream, MeshFile metaData);

Check failure on line 8 in src/ServiceLayer.Mesh/IFileTransformer.cs

View workflow job for this annotation

GitHub Actions / Test stage / Unit tests

The type or namespace name 'MeshFile' could not be found (are you missing a using directive or an assembly reference?)
}
1 change: 1 addition & 0 deletions src/ServiceLayer.Mesh/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
services.AddTransient<IFileTransformQueueClientConfiguration, AppConfiguration>();
services.AddTransient<IMeshHandshakeFunctionConfiguration, AppConfiguration>();
services.AddTransient<IFileRetryFunctionConfiguration, AppConfiguration>();
services.AddTransient<IFileTransformFunctionConfiguration, AppConfiguration>();
});


Expand Down
5 changes: 3 additions & 2 deletions src/ServiceLayer.Mesh/Storage/MeshFilesBlobStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ namespace ServiceLayer.Mesh.Storage;

public class MeshFilesBlobStore(BlobContainerClient blobContainerClient) : IMeshFilesBlobStore
{
public Task<Stream> DownloadAsync(MeshFile file)
public async Task<Stream> DownloadAsync(MeshFile file)
{
throw new NotImplementedException();
var blobClient = blobContainerClient.GetBlobClient(file.BlobPath);
return (await blobClient.DownloadAsync()).Value.Content;
}

public async Task<string> UploadAsync(MeshFile file, byte[] data)
Expand Down
12 changes: 12 additions & 0 deletions src/ServiceLayer.Mesh/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ServiceLayer.Mesh;

public class ValidationError
{
public int? RowNumber { get; set; }

public required string Field { get; set; }

public required string Code { get; set; }

public required string Error { get; set; }
}
Loading
Loading