Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2c3817e
Merge pull request #9 from ameysunu/dev
ameysunu Oct 23, 2025
f6af1eb
if state DONE then return NoContent added
ameysunu Oct 23, 2025
1a18061
Merge remote-tracking branch 'origin/master'
ameysunu Oct 23, 2025
3892751
try catch and google auth added for url signing
ameysunu Oct 24, 2025
2165df9
get job state by user id added
ameysunu Oct 25, 2025
c507d0e
ui changes
ameysunu Oct 25, 2025
e5c6a65
vercel added for routing
ameysunu Oct 25, 2025
4486621
new sinks created
ameysunu Oct 25, 2025
f508a15
parquet added to factory
ameysunu Oct 25, 2025
f8011a2
rate limiter added
ameysunu Oct 26, 2025
cc0e69c
display error messages on ui
ameysunu Oct 26, 2025
095f19b
fallback to firebase uid added on ui
ameysunu Oct 27, 2025
b285aff
refresh button added
Oct 30, 2025
a616729
ui improvments
Oct 30, 2025
674ba52
Add Error property to JobsDocument model
ameysunu Oct 30, 2025
db9b475
firebase error added to ui
Oct 30, 2025
272b686
Merge branch 'master' of https://github.com/ameysunu/DataMorph
Oct 30, 2025
466b8b7
error message added to job state on fail
ameysunu Oct 30, 2025
0204098
_logger added in ParsingApi
ameysunu Oct 30, 2025
b55a615
footer added
Oct 30, 2025
7f7d435
rate limiter changed from 15 minutes to 30 seconds
ameysunu Oct 30, 2025
812c769
increased rate limiter from 30 to 60 seconds
ameysunu Oct 30, 2025
fd53c07
README.md added
ameysunu Oct 30, 2025
611de7d
README updated
ameysunu Oct 30, 2025
6caa097
mermaid architecture added to README
ameysunu Oct 30, 2025
3c1754e
Update contributing guidelines in README
ameysunu Oct 30, 2025
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
2 changes: 2 additions & 0 deletions Api/Abstractions/IFirestoreService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json.Nodes;
using Api.Models;
using Google.Cloud.Firestore;

namespace Api.Abstractions;

Expand All @@ -8,4 +9,5 @@ public interface IFirestoreService
Task CreateInitAsync(string jobId, string userId, string rawPath, JsonObject? targetSchema, string? prompt, JsonObject? options);
Task<Dictionary<string, object>?> GetAsync(string jobId);
Task UpdateStateAsync(string jobId, JobState state, Dictionary<string, object?>? extra = null);
Task<IReadOnlyList<DocumentSnapshot>> GetDocumentsByField(string fieldName, string fieldValue);
}
8 changes: 8 additions & 0 deletions Api/Abstractions/IRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Api.Abstractions;

public interface IRateLimiter
{
Task<bool> GetUserLimitsAsync(string userId);
Task<bool> CheckUserLimit(string userId);
Task UpdateOrCreateUserLimitAsync(string userId);
}
44 changes: 44 additions & 0 deletions Api/Api/JobLists.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Api.Abstractions;
using Api.Services;
using Google.Cloud.Firestore;
using Microsoft.AspNetCore.Mvc;

namespace Api.Api;

[ApiController]
[Route("api/jobs")]
public class JobLists: ControllerBase
{
private readonly ILogger<JobLists> _logger;
private readonly IFirestoreService _firestoreService;

public JobLists(
ILogger<JobLists> logger,
IFirestoreService firestoreService
)
{
_logger = logger;
_firestoreService = firestoreService;
}

[HttpGet]
public async Task <IActionResult> GetJobByUserId([FromQuery] string userId)
{
if (string.IsNullOrWhiteSpace(userId))
return Ok(Array.Empty<object>());

List<JobsDocument> jobList = new();
var documents = await _firestoreService.GetDocumentsByField("userId", userId);

if (documents is null || documents.Count == 0)
return Ok(jobList);

foreach (var doc in documents)
{
var job = doc.ConvertTo<JobsDocument>();
jobList.Add(job);
}

return Ok(jobList);
}
}
7 changes: 5 additions & 2 deletions Api/Api/Pipelines/Initialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ public class Initialize:
private readonly IFirestoreService _firestoreService;
private readonly IGcsSigner _gcsSigner;
private readonly ApiConfig _apiConfig;
private readonly IRateLimiter _rateLimiter;

public Initialize(
IFirestoreService firestoreService,
IGcsSigner gcsSigner,
IOptions<ApiConfig> apiConfig
IOptions<ApiConfig> apiConfig,
IRateLimiter rateLimiter
)
{
_firestoreService = firestoreService;
_gcsSigner = gcsSigner;
_apiConfig = apiConfig.Value;
_rateLimiter = rateLimiter;
}

[HttpPost("init")]
public virtual async Task<IActionResult> Init([FromBody] InitRequest request)
=> await Initialize(request, _firestoreService, _gcsSigner, _apiConfig);
=> await Initialize(request, _firestoreService, _gcsSigner, _rateLimiter, _apiConfig);

[HttpGet("init")]
public virtual IActionResult GetNotAllowed()
Expand Down
5 changes: 5 additions & 0 deletions Api/Api/Pipelines/InitializeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ protected async Task<IActionResult> Initialize(
[FromBody] InitRequest request,
IFirestoreService firestoreService,
IGcsSigner gcsSigner,
IRateLimiter rateLimiter,
ApiConfig apiConfig)
{
var rateLimitResult = await rateLimiter.CheckUserLimit(request.UserId);
if (rateLimitResult)
return StatusCode(StatusCodes.Status429TooManyRequests, new {error = "To prevent abuse, we have rate limited your requests. Please try again later."});

if (string.IsNullOrWhiteSpace(request.FileName))
return BadRequest(new {error = "File Name is required"});

Expand Down
1 change: 1 addition & 0 deletions Api/DependencyInjection/AddApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static IServiceCollection AddApiServices( this IServiceCollection service
services.AddSingleton<IPipelines<InitRequest>, Initialize>();
services.AddSingleton<IPipelines<JobStatusRequest>, JobStatus>();
services.AddSingleton<IHealth, HealthChecks>();
services.AddSingleton<IRateLimiter, RateLimiter>();

return services;
}
Expand Down
1 change: 1 addition & 0 deletions Api/Models/ApiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public class ApiConfig
public string ProjectId { get; set; }
public string BucketName { get; set; }
public string FirestoreCollection { get; set; }
public string FirestoreRateCollection { get; set; }
public string ServiceAccountKeyPath { get; set; }
}
26 changes: 26 additions & 0 deletions Api/Models/JobsDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Google.Cloud.Firestore;

[FirestoreData]
public class JobsDocument
{
[FirestoreProperty("jobId")]
public string JobId { get; set; }

[FirestoreProperty("userId")]
public string UserId { get; set; }

[FirestoreProperty("state")]
public string State { get; set; }

[FirestoreProperty("createdAt")]
public DateTime CreatedAt { get; set; }

[FirestoreProperty("updatedAt")]
public DateTime UpdatedAt { get; set; }

[FirestoreProperty("result")]
public object Result { get; set; }

[FirestoreProperty("error")]
public string? Error { get; set; }
}
9 changes: 9 additions & 0 deletions Api/Services/FirestoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ public async Task CreateInitAsync( string jobId,
return snap.Exists ? snap.ToDictionary() : null;
}

public async Task<IReadOnlyList<DocumentSnapshot>> GetDocumentsByField(string fieldName, string fieldValue)
{
var querySnap = await _collection.WhereEqualTo(fieldName, fieldValue).GetSnapshotAsync();
if(querySnap == null || querySnap.Documents.Count == 0)
return null;
return querySnap.Documents;

}

public async Task UpdateStateAsync(string jobId, JobState state, Dictionary<string, object?>? extra = null)
{
var updates = new Dictionary<string, object?>
Expand Down
72 changes: 72 additions & 0 deletions Api/Services/RateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Api.Abstractions;
using Api.Models;
using Google.Cloud.Firestore;
using Microsoft.Extensions.Options;

namespace Api.Services;

public class RateLimiter: IRateLimiter
{
private readonly CollectionReference _collection;
private readonly ApiConfig _apiConfig;

public RateLimiter(IOptions<ApiConfig> apiConfig)
{
_apiConfig = apiConfig.Value;
var db = FirestoreDb.Create(_apiConfig.ProjectId);
_collection = db.Collection(_apiConfig.FirestoreRateCollection);
}

public async Task<bool> CheckUserLimit(string userId)
{
var userLimits = await GetUserLimitsAsync(userId);
if (userLimits)
{
return true;
}

return false;
}

public async Task<bool> GetUserLimitsAsync(string userId)
{
var snap = await _collection.Document(userId).GetSnapshotAsync();
if (!snap.Exists)
{
await UpdateOrCreateUserLimitAsync(userId);
return true;
}


if (snap.TryGetValue("lastUpdate", out Timestamp lastUpdateTimestamp))
{
var lastUpdated = lastUpdateTimestamp.ToDateTime().ToUniversalTime();
var now = DateTime.UtcNow;

if (now - lastUpdated > TimeSpan.FromSeconds(60))
{
await UpdateOrCreateUserLimitAsync(userId);
return false;
}

return true;
}

await UpdateOrCreateUserLimitAsync(userId);
return true;
}

public async Task UpdateOrCreateUserLimitAsync(string userId)
{
var doc = _collection.Document(userId);
var now = DateTime.UtcNow;
var data = new Dictionary<string, object>
{
["userId"] = userId,
["lastUpdate"] = now
};

await doc.SetAsync(data);
}

}
10 changes: 10 additions & 0 deletions Parser/Parser/Abstractions/IOutputSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Parser.Abstractions;

public interface IOutputSink: IAsyncDisposable
{
Task<int> WriteChunkAsync(StreamWriter writer, string transformedChunk);
int TotalRows { get; }
Stream GetFinalStream();
string FileExtension { get; }
string ContentType { get; }
}
6 changes: 6 additions & 0 deletions Parser/Parser/Abstractions/IOutputSinkFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Parser.Abstractions;

public interface IOutputSinkFactory
{
IOutputSink Create(string formatHint);
}
1 change: 1 addition & 0 deletions Parser/Parser/Abstractions/ITransformationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface ITransformationService
{
Task<string> CallGeminiForChunkAsync(List<string> jsonlChunk, string userPrompt);
Task<int> WriteJsonlAsync(StreamWriter writer, string modelOutput);
Task<string> GetValueFromGeminiAsync(string systemInstruction, string userPrompt);
}
Loading