Skip to content

Commit

Permalink
Use project ID, not code, in CrdtMerge API (#1169)
Browse files Browse the repository at this point in the history
* Revisit filenames we use for CrdtMerge

Use project code and ID in root folder name, use "crdt" and "fw" inside
root folder no matter what the project's actual code or name is.

* Update CRDT caching so same filename isn't a problem

Using the name "crdt" everywhere as a project name caused a couple of
caching issues in existing CRDT code, but the change is pretty easy.

* Now use project ID, not code, in CrdtMerge API

Old URL: /sync?projectCode=sena-3
New URL: /sync?projectId=(guid)

---------

Co-authored-by: Kevin Hahn <[email protected]>
  • Loading branch information
rmunn and hahn-kev authored Oct 28, 2024
1 parent f72f3e0 commit baf6cd5
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 30 deletions.
1 change: 1 addition & 0 deletions backend/CrdtMerge/CrdtMerge.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ProjectReference Include="../FwLite/LcmCrdt/LcmCrdt.csproj" />
<ProjectReference Include="../FwLite/FwLiteProjectSync/FwLiteProjectSync.csproj" />
<ProjectReference Include="../FixFwData/FixFwData.csproj" />
<ProjectReference Include="../LexData/LexData.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions backend/CrdtMerge/CrdtMergeKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static void AddCrdtMerge(this IServiceCollection services)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddScoped<SendReceiveService>();
services.AddScoped<ProjectLookupService>();
services
.AddLcmCrdtClient()
.AddFwDataBridge()
Expand Down
46 changes: 29 additions & 17 deletions backend/CrdtMerge/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using FwDataMiniLcmBridge;
using FwLiteProjectSync;
using LcmCrdt;
using LexData;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
using MiniLcm;
using Scalar.AspNetCore;
Expand All @@ -12,6 +14,11 @@
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.Services.AddLexData(
autoApplyMigrations: false,
useOpenIddict: false
);

builder.Services.AddCrdtMerge();

var app = builder.Build();
Expand All @@ -30,56 +37,61 @@

app.Run();

static async Task<CrdtFwdataProjectSyncService.SyncResult> ExecuteMergeRequest(
static async Task<Results<Ok<CrdtFwdataProjectSyncService.SyncResult>, NotFound>> ExecuteMergeRequest(
ILogger<Program> logger,
IServiceProvider services,
SendReceiveService srService,
IOptions<CrdtMergeConfig> config,
FwDataFactory fwDataFactory,
ProjectsService projectsService,
ProjectLookupService projectLookupService,
CrdtFwdataProjectSyncService syncService,
string projectCode,
// string projectName, // TODO: Add this to the API eventually
Guid projectId,
bool dryRun = false)
{
logger.LogInformation("About to execute sync request for {projectCode}", projectCode);
logger.LogInformation("About to execute sync request for {projectId}", projectId);
if (dryRun)
{
logger.LogInformation("Dry run, not actually syncing");
return new(0, 0);
return TypedResults.Ok(new CrdtFwdataProjectSyncService.SyncResult(0, 0));
}

var projectCode = await projectLookupService.GetProjectCode(projectId);
if (projectCode is null)
{
logger.LogError("Project ID {projectId} not found", projectId);
return TypedResults.NotFound();
}
logger.LogInformation("Project code is {projectCode}", projectCode);

// TODO: Instead of projectCode here, we'll evetually look up project ID and use $"{projectName}-{projectId}" as the project folder
var projectFolder = Path.Join(config.Value.ProjectStorageRoot, projectCode);
var projectFolder = Path.Join(config.Value.ProjectStorageRoot, $"{projectCode}-{projectId}");
if (!Directory.Exists(projectFolder)) Directory.CreateDirectory(projectFolder);

// TODO: add projectName parameter and use it instead of projectCode here
var crdtFile = Path.Join(projectFolder, $"{projectCode}.sqlite");
var crdtFile = Path.Join(projectFolder, "crdt.sqlite");

var fwDataProject = new FwDataProject(projectCode, projectFolder); // TODO: use projectName (once we have it) instead of projectCode here
var fwDataProject = new FwDataProject("fw", projectFolder);
logger.LogDebug("crdtFile: {crdtFile}", crdtFile);
logger.LogDebug("fwDataFile: {fwDataFile}", fwDataProject.FilePath);

if (File.Exists(fwDataProject.FilePath))
{
var srResult = srService.SendReceive(fwDataProject);
var srResult = srService.SendReceive(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result: {srResult}", srResult.Output);
}
else
{
var srResult = srService.Clone(fwDataProject);
var srResult = srService.Clone(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result: {srResult}", srResult.Output);
}
var fwdataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, true);
// var crdtProject = projectsService.GetProject(crdtProjectName);
var crdtProject = File.Exists(crdtFile) ?
new CrdtProject(projectCode, crdtFile) : // TODO: use projectName (once we have it) instead of projectCode here
await projectsService.CreateProject(new(projectCode, SeedNewProjectData: false, Path: projectFolder, FwProjectId: fwdataApi.ProjectId));
new CrdtProject("crdt", crdtFile) :
await projectsService.CreateProject(new("crdt", SeedNewProjectData: false, Path: projectFolder, FwProjectId: fwdataApi.ProjectId));
var miniLcmApi = await services.OpenCrdtProject(crdtProject);
var result = await syncService.Sync(miniLcmApi, fwdataApi, dryRun);
logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}", result.CrdtChanges, result.FwdataChanges);
var srResult2 = srService.SendReceive(fwDataProject);
var srResult2 = srService.SendReceive(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result after CRDT sync: {srResult2}", srResult2.Output);
return result;
return TypedResults.Ok(result);
}

16 changes: 16 additions & 0 deletions backend/CrdtMerge/ProjectLookupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using LexData;
using Microsoft.EntityFrameworkCore;

namespace CrdtMerge;

public class ProjectLookupService(LexBoxDbContext dbContext)
{
public async ValueTask<string?> GetProjectCode(Guid projectId)
{
var projectCode = await dbContext.Projects
.Where(p => p.Id == projectId)
.Select(p => p.Code)
.FirstOrDefaultAsync();
return projectCode;
}
}
12 changes: 6 additions & 6 deletions backend/CrdtMerge/SendReceiveHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ private static Uri BuildSendReceiveUrl(string baseUrl, string projectCode, SendR
return builder.Uri;
}

public static LfMergeBridgeResult SendReceive(FwDataProject project, string baseUrl = "http://localhost", SendReceiveAuth? auth = null, string fdoDataModelVersion = "7000072", string? commitMessage = null)
public static LfMergeBridgeResult SendReceive(FwDataProject project, string? projectCode = null, string baseUrl = "http://localhost", SendReceiveAuth? auth = null, string fdoDataModelVersion = "7000072", string? commitMessage = null)
{
// If projectCode not given, calculate it from the fwdataPath
projectCode ??= project.Name;
var fwdataInfo = new FileInfo(project.FilePath);
if (fwdataInfo.Directory is null) throw new InvalidOperationException(
$"Not allowed to Send/Receive root-level directories like C:\\, was '{project.FilePath}'");

var repoUrl = BuildSendReceiveUrl(baseUrl, project.Name, auth);
var repoUrl = BuildSendReceiveUrl(baseUrl, projectCode, auth);

var flexBridgeOptions = new Dictionary<string, string>
{
Expand All @@ -68,13 +68,13 @@ public static LfMergeBridgeResult SendReceive(FwDataProject project, string base
return CallLfMergeBridge("Language_Forge_Send_Receive", flexBridgeOptions);
}

public static LfMergeBridgeResult CloneProject(FwDataProject project, string baseUrl = "http://localhost", SendReceiveAuth? auth = null, string fdoDataModelVersion = "7000072")
public static LfMergeBridgeResult CloneProject(FwDataProject project, string? projectCode = null, string baseUrl = "http://localhost", SendReceiveAuth? auth = null, string fdoDataModelVersion = "7000072")
{
// If projectCode not given, calculate it from the fwdataPath
projectCode ??= project.Name;
var fwdataInfo = new FileInfo(project.FilePath);
if (fwdataInfo.Directory is null) throw new InvalidOperationException($"Not allowed to Send/Receive root-level directories like C:\\ '{project.FilePath}'");

var repoUrl = BuildSendReceiveUrl(baseUrl, project.Name, auth);
var repoUrl = BuildSendReceiveUrl(baseUrl, projectCode, auth);

var flexBridgeOptions = new Dictionary<string, string>
{
Expand Down
6 changes: 4 additions & 2 deletions backend/CrdtMerge/SendReceiveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ namespace CrdtMerge;

public class SendReceiveService(IOptions<CrdtMergeConfig> config)
{
public SendReceiveHelpers.LfMergeBridgeResult SendReceive(FwDataProject project, string? commitMessage = null)
public SendReceiveHelpers.LfMergeBridgeResult SendReceive(FwDataProject project, string? projectCode, string? commitMessage = null)
{
return SendReceiveHelpers.SendReceive(
project: project,
projectCode: projectCode,
baseUrl: config.Value.HgWebUrl,
auth: new SendReceiveHelpers.SendReceiveAuth(config.Value),
fdoDataModelVersion: config.Value.FdoDataModelVersion,
commitMessage: commitMessage
);
}

public SendReceiveHelpers.LfMergeBridgeResult Clone(FwDataProject project)
public SendReceiveHelpers.LfMergeBridgeResult Clone(FwDataProject project, string? projectCode)
{
return SendReceiveHelpers.CloneProject(
project: project,
projectCode: projectCode,
baseUrl: config.Value.HgWebUrl,
auth: new SendReceiveHelpers.SendReceiveAuth(config.Value),
fdoDataModelVersion: config.Value.FdoDataModelVersion
Expand Down
3 changes: 3 additions & 0 deletions backend/CrdtMerge/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"LexboxPassword": "pass",
"FdoDataModelVersion": "7000072"
},
"DbConfig": {
"LexBoxConnectionString": "Host=localhost;Port=5433;Username=postgres;Password=972b722e63f549938d07bd8c4ee5086c;Database=lexbox;Include Error Detail=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public FwDataMiniLcmApi GetFwDataMiniLcmApi(string projectName, bool saveOnDispo
return GetFwDataMiniLcmApi(project, saveOnDispose);
}

private string CacheKey(FwDataProject project) => $"{nameof(FwDataFactory)}|{project.FileName}";
private string CacheKey(FwDataProject project) => $"{nameof(FwDataFactory)}|{project.FilePath}";

public FwDataMiniLcmApi GetFwDataMiniLcmApi(FwDataProject project, bool saveOnDispose)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public async Task<SyncResult> Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataA
}
var projectSnapshot = await GetProjectSnapshot(fwdataApi.Project.Name, fwdataApi.Project.ProjectsPath);
SyncResult result = await Sync(crdtApi, fwdataApi, dryRun, fwdataApi.EntryCount, projectSnapshot);
fwdataApi.Save();

if (!dryRun)
{
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Soenneker.Utils.AutoBogus" Version="2.1.278" />
Expand Down
26 changes: 26 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace LcmCrdt.Tests;

public class OpenProjectTests
{
[Fact]
public async Task OpeningAProjectWorks()
{
var sqliteConnectionString = "OpeningAProjectWorks.sqlite";
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Services.AddLcmCrdtClient();
using var host = builder.Build();
var services = host.Services;
var asyncScope = services.CreateAsyncScope();
await asyncScope.ServiceProvider.GetRequiredService<ProjectsService>()
.CreateProject(new(Name: "OpeningAProjectWorks", Path: ""));

var miniLcmApi = (CrdtMiniLcmApi)await asyncScope.ServiceProvider.OpenCrdtProject(new CrdtProject("OpeningAProjectWorks", sqliteConnectionString));
miniLcmApi.ProjectData.Name.Should().Be("OpeningAProjectWorks");

await asyncScope.ServiceProvider.GetRequiredService<LcmCrdtDbContext>().Database.EnsureDeletedAsync();
}
}
2 changes: 1 addition & 1 deletion backend/FwLite/LcmCrdt/CurrentProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async ValueTask<ProjectData> GetProjectData()

private static string CacheKey(CrdtProject project)
{
return project.Name + "|ProjectData";
return project.DbPath + "|ProjectData";
}

private static string CacheKey(Guid projectId)
Expand Down
10 changes: 9 additions & 1 deletion backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,18 @@ public static void ConfigureCrdt(CrdtConfig config)
.Add<CreateComplexFormType>();
}

public static async Task<IMiniLcmApi> OpenCrdtProject(this IServiceProvider services, CrdtProject project)
public static Task<IMiniLcmApi> OpenCrdtProject(this IServiceProvider services, CrdtProject project)
{
//this method must not be async, otherwise Setting the project scope will not work as expected.
//the project is stored in the async scope, if a new scope is created in this method then it will be gone once the method returns
//making the lcm api unusable
var projectsService = services.GetRequiredService<ProjectsService>();
projectsService.SetProjectScope(project);
return LoadMiniLcmApi(services);
}

private static async Task<IMiniLcmApi> LoadMiniLcmApi(IServiceProvider services)
{
await services.GetRequiredService<CurrentProjectService>().PopulateProjectDataCache();
return services.GetRequiredService<IMiniLcmApi>();
}
Expand Down
3 changes: 2 additions & 1 deletion backend/LexData/DataKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static class DataKernel
{
public static void AddLexData(this IServiceCollection services,
bool autoApplyMigrations,
bool useOpenIddict = true,
ServiceLifetime dbContextLifeTime = ServiceLifetime.Scoped)
{
services.AddScoped<SeedingData>();
Expand All @@ -17,7 +18,7 @@ public static void AddLexData(this IServiceCollection services,
options.EnableDetailedErrors();
options.UseNpgsql(serviceProvider.GetRequiredService<IOptions<DbConfig>>().Value.LexBoxConnectionString);
options.UseProjectables();
options.UseOpenIddict();
if (useOpenIddict) options.UseOpenIddict();
#if DEBUG
options.EnableSensitiveDataLogging();
#endif
Expand Down

0 comments on commit baf6cd5

Please sign in to comment.