test(S01/T01): Added a job-scoped Gmail matching contract with backend-…
- JobTrackerApi/Controllers/GmailController.cs - JobTrackerApi/Services/GmailOAuthService.cs - JobTrackerApi.Tests/GmailControllerTests.cs - .gsd/milestones/M001/slices/S01/S01-PLAN.md - .gsd/KNOWLEDGE.md
This commit is contained in:
@@ -19,11 +19,13 @@ public interface IGmailOAuthService
|
||||
Task DisconnectAsync(string ownerUserId, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<GmailMessageSummary>> ListMessagesAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<GmailMessageSummary>> ListMessagesForQueriesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<GmailQueryMatchedMessage>> ListJobCandidateMessagesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken);
|
||||
Task<GmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record GmailOAuthExchangeResult(string GmailAddress);
|
||||
public sealed record GmailMessageSummary(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet);
|
||||
public sealed record GmailQueryMatchedMessage(GmailMessageSummary Message, IReadOnlyList<string> MatchedQueries);
|
||||
public sealed record GmailMessageDetail(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet, string BodyText, string? BodyHtml);
|
||||
|
||||
internal sealed class GmailTokenResponse
|
||||
@@ -182,6 +184,12 @@ public sealed class GmailOAuthService : IGmailOAuthService
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<GmailMessageSummary>> ListMessagesForQueriesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken)
|
||||
{
|
||||
var matchedMessages = await ListJobCandidateMessagesAsync(ownerUserId, queries, maxResultsPerQuery, cancellationToken);
|
||||
return matchedMessages.Select(static item => item.Message).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<GmailQueryMatchedMessage>> ListJobCandidateMessagesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken)
|
||||
{
|
||||
maxResultsPerQuery = Math.Clamp(maxResultsPerQuery, 1, 25);
|
||||
var normalizedQueries = queries
|
||||
@@ -192,19 +200,28 @@ public sealed class GmailOAuthService : IGmailOAuthService
|
||||
|
||||
if (normalizedQueries.Count == 0)
|
||||
{
|
||||
return Array.Empty<GmailMessageSummary>();
|
||||
return Array.Empty<GmailQueryMatchedMessage>();
|
||||
}
|
||||
|
||||
var combined = new List<GmailMessageSummary>();
|
||||
var combined = new Dictionary<string, (GmailMessageSummary Message, HashSet<string> Queries)>(StringComparer.Ordinal);
|
||||
foreach (var query in normalizedQueries)
|
||||
{
|
||||
var items = await ListMessagesAsync(ownerUserId, query, maxResultsPerQuery, cancellationToken);
|
||||
combined.AddRange(items);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!combined.TryGetValue(item.Id, out var existing))
|
||||
{
|
||||
combined[item.Id] = (item, new HashSet<string>(StringComparer.OrdinalIgnoreCase) { query });
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.Queries.Add(query);
|
||||
combined[item.Id] = existing;
|
||||
}
|
||||
}
|
||||
|
||||
return combined
|
||||
.GroupBy(message => message.Id, StringComparer.Ordinal)
|
||||
.Select(group => group.First())
|
||||
return combined.Values
|
||||
.Select(static entry => new GmailQueryMatchedMessage(entry.Message, entry.Queries.OrderBy(static query => query, StringComparer.OrdinalIgnoreCase).ToList()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user