Skip to content

Commit cb79f94

Browse files
author
Calin Lupas
authored
Merge pull request #29 from DevExcelerate/feature/add-sqlprovider
Add sql provider and clean up code
2 parents 5caa6e9 + 9fd3cf5 commit cb79f94

20 files changed

Lines changed: 456 additions & 384 deletions
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using Octokit.Helpers;
2+
using Octokit;
3+
4+
namespace DevExcelerateApi.Core.Extensions;
5+
6+
public static class GitHubClientExtensions
7+
{
8+
public static async Task CopyFilesAsync(this IGitHubClient gitHubClient, string sourceOwner, string sourceRepo, string sourcePath, string targetOwner, string targetRepo, string targetPath)
9+
{
10+
if (gitHubClient == null)
11+
{
12+
throw new InvalidOperationException("GitHub client is not configured.");
13+
}
14+
15+
var sourceFiles = await gitHubClient.Repository.Content.GetAllContents(sourceOwner, sourceRepo, sourcePath);
16+
17+
foreach (var file in sourceFiles)
18+
{
19+
if (file.Type == ContentType.File)
20+
{
21+
var fileContent = await gitHubClient.Repository.Content.GetRawContent(sourceOwner, sourceRepo, file.Path);
22+
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
23+
var createFileRequest = new CreateFileRequest($"Copying {file.Name} from {sourceRepo}", fileContentString);
24+
25+
var targetFilePath = $"{targetPath}/{file.Name}";
26+
await gitHubClient.Repository.Content.CreateFile(targetOwner, targetRepo, targetFilePath, createFileRequest);
27+
}
28+
}
29+
}
30+
31+
public static async Task<string> GetFileContentAsync(this IGitHubClient gitHubClient, string owner, string repo, string path)
32+
{
33+
if (gitHubClient == null)
34+
{
35+
throw new InvalidOperationException("GitHub client is not configured.");
36+
}
37+
38+
var fileContent = await gitHubClient.Repository.Content.GetRawContent(owner, repo, path);
39+
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
40+
return fileContentString;
41+
}
42+
43+
public static async Task<RepositoryContentChangeSet?> SaveFileAsync(
44+
this IGitHubClient gitHubClient,
45+
string owner,
46+
string repo,
47+
string defaultBranchName,
48+
string branch,
49+
string path,
50+
string content)
51+
{
52+
if (gitHubClient == null)
53+
{
54+
throw new InvalidOperationException("GitHub client is not configured.");
55+
}
56+
57+
try
58+
{
59+
await ReferenceExtensions.CreateBranch(gitHubClient.Git.Reference, owner, repo, branch, defaultBranchName);
60+
}
61+
catch (ApiValidationException ex)
62+
{
63+
// 422: Reference already exists
64+
if (ex.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
65+
{
66+
// Ignore the exception if the branch already exists
67+
}
68+
else
69+
{
70+
throw;
71+
}
72+
}
73+
74+
try
75+
{
76+
// Check if the file already exists
77+
var existingFile = await gitHubClient.Repository.Content.GetAllContentsByRef(owner, repo, path, branch);
78+
79+
if (existingFile != null && existingFile.Count > 0)
80+
{
81+
var updateFileRequest = new UpdateFileRequest($"Updating file {path}", content, existingFile[0].Sha, branch);
82+
return await gitHubClient.Repository.Content.UpdateFile(owner, repo, path, updateFileRequest);
83+
}
84+
}
85+
catch (NotFoundException)
86+
{
87+
var createFileRequest = new CreateFileRequest($"Adding file {path}", content, branch);
88+
89+
return await gitHubClient.Repository.Content.CreateFile(owner, repo, path, createFileRequest);
90+
}
91+
92+
return null;
93+
}
94+
95+
public static async Task<PullRequest> CreatePullRequestAsync(this IGitHubClient gitHubClient, string owner, string repo, string headBranch, string baseBranch, string title, string body)
96+
{
97+
if (gitHubClient == null)
98+
{
99+
throw new InvalidOperationException("GitHub client is not configured.");
100+
}
101+
102+
// Step 1: Check if a pull request already exists for the head branch
103+
var pullRequests = await gitHubClient.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest
104+
{
105+
State = ItemStateFilter.Open, // Only check open pull requests
106+
Head = $"{owner}:{headBranch}" // Filter by the head branch
107+
});
108+
109+
if (pullRequests.Any())
110+
{
111+
// If a pull request already exists, return the first one
112+
return pullRequests[0];
113+
}
114+
115+
// Step 2: Create a new pull request if none exists
116+
var newPullRequest = new NewPullRequest(title, headBranch, baseBranch)
117+
{
118+
Body = body
119+
};
120+
121+
return await gitHubClient.PullRequest.Create(owner, repo, newPullRequest);
122+
}
123+
124+
public static async Task<IReadOnlyList<GitHubCommitFile>> GetCommitFilesAsync(this IGitHubClient gitHubClient, string owner, string repo, string commitSha)
125+
{
126+
if (gitHubClient == null)
127+
{
128+
throw new InvalidOperationException("GitHub client is not configured.");
129+
}
130+
131+
// Fetch the commit details using the commit SHA
132+
var commit = await gitHubClient.Repository.Commit.Get(owner, repo, commitSha);
133+
134+
// Return the list of files associated with the commit
135+
return commit.Files;
136+
}
137+
138+
public static async Task<Dictionary<string, string>> GetCommitFilesWithContentAsync(this IGitHubClient gitHubClient, string owner, string repo, string commitSha)
139+
{
140+
if (gitHubClient == null)
141+
{
142+
throw new InvalidOperationException("GitHub client is not configured.");
143+
}
144+
145+
// Dictionary to store file paths and their content
146+
var fileContents = new Dictionary<string, string>();
147+
148+
var files = await GetCommitFilesAsync(gitHubClient, owner, repo, commitSha);
149+
150+
foreach (var file in files)
151+
{
152+
if (file.Status == "added" || file.Status == "modified") // Only fetch content for added/modified files
153+
{
154+
try
155+
{
156+
// Fetch the raw content of the file
157+
var rawContent = await gitHubClient.Repository.Content.GetRawContent(owner, repo, file.Filename);
158+
var contentString = System.Text.Encoding.UTF8.GetString(rawContent);
159+
160+
// Add the file path and content to the dictionary
161+
fileContents.Add(file.Filename, contentString);
162+
}
163+
catch (NotFoundException)
164+
{
165+
// Handle cases where the file content is not accessible
166+
fileContents.Add(file.Filename, "Content not accessible");
167+
}
168+
}
169+
}
170+
171+
return fileContents;
172+
}
173+
}

src/DevExcelerateApi/Core/Extensions/ServiceExtensions.cs

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ public static class ServiceExtensions
1818
internal static IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration)
1919
{
2020
// GitHub App configuration
21-
AddOptions<GitHubOptions>(GitHubOptions.ConfigurationSectionName);
21+
AddOptionsLocal<GitHubOptions>(GitHubOptions.ConfigurationSectionName);
2222

2323
// Azure configuration
24-
AddOptions<AzureOptions>(AzureOptions.ConfigurationSectionName);
24+
AddOptionsLocal<AzureOptions>(AzureOptions.ConfigurationSectionName);
2525

2626
// Data Storage configuration
27-
AddOptions<DataStoreOptions>(DataStoreOptions.ConfigurationSectionName);
27+
AddOptionsLocal<DataStoreOptions>(DataStoreOptions.ConfigurationSectionName);
2828

2929
// Add new configuration sections here
3030

31-
void AddOptions<TOptions>(string propertyName) where TOptions : class
31+
void AddOptionsLocal<TOptions>(string propertyName) where TOptions : class
3232
{
3333
services.AddOptions<TOptions>(configuration.GetSection(propertyName));
3434
}
@@ -91,7 +91,6 @@ internal static IServiceCollection AddAppServices(this IServiceCollection servic
9191

9292
// Add DevEx services
9393
services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
94-
services.AddSingleton<IGitHubHelper, GitHubHelper>();
9594
services.AddSingleton<IDevExIssuesEventProcessorService, DevExIssuesEventProcessorService>();
9695
services.AddSingleton<IDevExPullRequestEventProcessorService, DevExPullRequestEventProcessorService>();
9796
services.AddSingleton<WebhookEventProcessor, DevExWebhookEventProcessorService>();
@@ -103,67 +102,74 @@ internal static IServiceCollection AddAppServices(this IServiceCollection servic
103102

104103
internal static IServiceCollection AddCorsPolicy(this IServiceCollection services, IConfiguration configuration)
105104
{
106-
string[] allowedOrigins = configuration.GetSection("AllowedOrigins").Get<string[]>() ?? [];
107-
if (allowedOrigins.Length > 0)
105+
var allowedOrigins = configuration.GetSection("AllowedOrigins").Get<string[]>() ?? [];
106+
if (allowedOrigins.Length == 0)
108107
{
109-
services.AddCors(options =>
110-
{
111-
options.AddDefaultPolicy(
112-
policy =>
113-
{
114-
policy.WithOrigins(allowedOrigins)
115-
.WithMethods("POST", "GET", "PUT", "DELETE", "PATCH")
116-
.AllowAnyHeader();
117-
});
118-
});
108+
return services;
119109
}
110+
111+
services.AddCors(options =>
112+
{
113+
options.AddDefaultPolicy(
114+
policy =>
115+
{
116+
policy.WithOrigins(allowedOrigins)
117+
.WithMethods("POST", "GET", "PUT", "DELETE", "PATCH")
118+
.AllowAnyHeader();
119+
});
120+
});
121+
120122
return services;
121123
}
122124

123125
internal static IServiceCollection AddDataStore(this IServiceCollection services)
124126
{
125-
IStorageContext<GitHubWebhookEntity> githubWebhookStorageContext;
126-
127-
DataStoreOptions dataStoreOptions = services.BuildServiceProvider().GetRequiredService<IOptions<DataStoreOptions>>().Value;
128-
129-
switch (dataStoreOptions.Type)
127+
services.AddSingleton(sp=>
130128
{
131-
case DataStoreOptions.DataStoreType.FileSystem:
132-
133-
if (dataStoreOptions.FileSystem == null)
134-
{
135-
throw new InvalidOperationException("DataStore:FileSystem is required when DataStore:Type is 'FileSystem'");
136-
}
137-
138-
string fullPath = Path.GetFullPath(dataStoreOptions.FileSystem.FilePath);
139-
string directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
140-
141-
githubWebhookStorageContext = new FileSystemStorageContext<GitHubWebhookEntity>(
142-
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_GitHubWebhooks{Path.GetExtension(fullPath)}")));
143-
144-
break;
145-
146-
case DataStoreOptions.DataStoreType.CosmosDb:
129+
IStorageContext<GitHubWebhookEntity> githubWebhookStorageContext;
130+
DataStoreOptions dataStoreOptions = sp.GetRequiredService<IOptions<DataStoreOptions>>().Value;
131+
switch (dataStoreOptions.Type)
132+
{
133+
case DataStoreOptions.DataStoreType.FileSystem:
147134

148-
if (dataStoreOptions.CosmosDb == null)
149-
{
150-
throw new InvalidOperationException("DataStore:CosmosDb is required when DataStore:Type is 'CosmosDb'");
151-
}
135+
if (dataStoreOptions.FileSystem == null)
136+
{
137+
throw new InvalidOperationException("DataStore:FileSystem is required when DataStore:Type is 'FileSystem'");
138+
}
152139

153-
githubWebhookStorageContext = new CosmosDbStorageContext<GitHubWebhookEntity>(
154-
dataStoreOptions.CosmosDb.ConnectionString,
155-
dataStoreOptions.CosmosDb.Database,
156-
dataStoreOptions.CosmosDb.GitHubWebhooksContainer);
140+
var fullPath = Path.GetFullPath(dataStoreOptions.FileSystem.FilePath);
141+
var directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
157142

158-
break;
143+
githubWebhookStorageContext = new FileSystemStorageContext<GitHubWebhookEntity>(
144+
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_GitHubWebhooks{Path.GetExtension(fullPath)}")));
145+
break;
146+
case DataStoreOptions.DataStoreType.CosmosDb:
159147

160-
default:
148+
if (dataStoreOptions.CosmosDb == null)
149+
{
150+
throw new InvalidOperationException("DataStore:CosmosDb is required when DataStore:Type is 'CosmosDb'");
151+
}
152+
153+
githubWebhookStorageContext = new CosmosDbStorageContext<GitHubWebhookEntity>(
154+
dataStoreOptions.CosmosDb.ConnectionString,
155+
dataStoreOptions.CosmosDb.Database,
156+
dataStoreOptions.CosmosDb.GitHubWebhooksContainer);
157+
break;
158+
case DataStoreOptions.DataStoreType.SqlServer:
159+
if (dataStoreOptions.SqlDb == null)
160+
{
161+
throw new InvalidOperationException("DataStore:SqlDb is required when DataStore:Type is 'SqlServer'");
162+
}
163+
githubWebhookStorageContext = new SqlDbStorageContext<GitHubWebhookEntity>(dataStoreOptions.SqlDb!);
164+
break;
165+
default:
161166
{
162167
throw new InvalidOperationException("Invalid 'DataStore' setting 'dataStoreOptions.Type'.");
163168
}
164-
}
169+
}
165170

166-
services.AddSingleton(new GitHubWebhookRepository(githubWebhookStorageContext));
171+
return new GitHubWebhookRepository(githubWebhookStorageContext);
172+
});
167173

168174
return services;
169175
}

src/DevExcelerateApi/Core/Options/DataStoreOptions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ public enum DataStoreType
2121
/// </summary>
2222
CosmosDb,
2323
/// <summary>
24-
/// Represents a SQLite data store.
25-
/// </summary>
26-
Sqlite,
27-
/// <summary>
2824
/// Represents a SQL Server data store.
2925
/// </summary>
3026
SqlServer
@@ -44,5 +40,10 @@ public enum DataStoreType
4440
/// Gets or sets the configuration for the Azure CosmosDB chat store.
4541
/// </summary>
4642
public CosmosDbOptions? CosmosDb { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the configuration for the Sql Db.
46+
/// </summary>
47+
public SqlDbOptions? SqlDb { get; set; }
4748
}
4849
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace DevExcelerateApi.Core.Options;
2+
3+
public class SqlDbOptions
4+
{
5+
/// <summary>
6+
/// Gets or sets the connection string
7+
/// </summary>
8+
public string? ConnectionString { get; set; }
9+
}

src/DevExcelerateApi/Data/CosmosDbStorageContext.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ namespace DevExcelerateApi.Data
99
/// <typeparam name="T"></typeparam>
1010
public class CosmosDbStorageContext<T> : IStorageContext<T>, IDisposable where T : IStorageEntity
1111
{
12-
// Members & Constructors
13-
1412
// https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/migrate-dotnet-v3?tabs=dotnet-v3
1513

1614
/// <summary>
@@ -21,7 +19,7 @@ public class CosmosDbStorageContext<T> : IStorageContext<T>, IDisposable where T
2119
/// <summary>
2220
/// CosmosDB container.
2321
/// </summary>
24-
protected readonly Container _container;
22+
private readonly Container _container;
2523

2624
/// <summary>
2725
/// Initializes a new instance of the CosmosDbContext class.
@@ -43,8 +41,6 @@ public CosmosDbStorageContext(string connectionString, string database, string c
4341
_container = _client.GetContainer(database, container);
4442
}
4543

46-
// Implementation of IStorageContext<T>
47-
4844
/// <inheritdoc/>
4945
public async Task<IEnumerable<T>> QueryEntitiesAsync(Func<T, bool> predicate)
5046
{
@@ -105,8 +101,6 @@ public async Task DeleteAsync(T entity)
105101
await _container.DeleteItemAsync<T>(entity.Id, new PartitionKey(entity.Partition));
106102
}
107103

108-
// IDisposable Members
109-
110104
public void Dispose()
111105
{
112106
Dispose(true);

0 commit comments

Comments
 (0)