Skip to content

Commit

Permalink
Standardize FW Lite ProjectData.Id (#1170)
Browse files Browse the repository at this point in the history
* standardize on ProjectData.Id being the lexbox Id, add a property to track the FieldWorks project Id. when doing a sync the fw project ids must match otherwise there's an error

* use OpenCrdtProject helper in SyncFixture
isolate SyncFailsWithMismatchedProjectIds test to not break other tests

* Update expected data model in snapshot tests

---------

Co-authored-by: Robin Munn <[email protected]>
  • Loading branch information
hahn-kev and rmunn authored Oct 28, 2024
1 parent 6a0d473 commit f72f3e0
Show file tree
Hide file tree
Showing 12 changed files with 58 additions and 19 deletions.
2 changes: 1 addition & 1 deletion backend/CrdtMerge/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
// 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, fwdataApi.ProjectId, SeedNewProjectData: false, Path: projectFolder));
await projectsService.CreateProject(new(projectCode, 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);
Expand Down
20 changes: 11 additions & 9 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FwDataMiniLcmBridge;
using System.Runtime.CompilerServices;
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.LcmUtils;
using FwDataMiniLcmBridge.Tests.Fixtures;
Expand All @@ -17,15 +18,18 @@ public class SyncFixture : IAsyncLifetime

public CrdtFwdataProjectSyncService SyncService =>
_services.ServiceProvider.GetRequiredService<CrdtFwdataProjectSyncService>();
public IServiceProvider Services => _services.ServiceProvider;
private readonly string _projectName;
private readonly MockProjectContext _projectContext = new(null);

public static SyncFixture Create(string projectName) => new(projectName);
public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName);

private SyncFixture(string projectName)
{
_projectName = projectName;
var crdtServices = new ServiceCollection()
.AddLcmCrdtClient()
.AddSingleton<ProjectContext>(_projectContext)
.AddTestFwDataBridge()
.AddFwLiteProjectSync()
.Configure<FwDataBridgeConfig>(c => c.ProjectsFolder = Path.Combine(".", _projectName, "FwData"))
Expand All @@ -45,31 +49,29 @@ public async Task InitializeAsync()
.ProjectsFolder;
if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true);
Directory.CreateDirectory(projectsFolder);
var lcmCache = _services.ServiceProvider.GetRequiredService<IProjectLoader>()
_services.ServiceProvider.GetRequiredService<IProjectLoader>()
.NewProject(new FwDataProject(_projectName, projectsFolder), "en", "fr");
var projectGuid = lcmCache.LanguageProject.Guid;
FwDataApi = _services.ServiceProvider.GetRequiredService<FwDataFactory>().GetFwDataMiniLcmApi(_projectName, false);

var crdtProjectsFolder =
_services.ServiceProvider.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
Directory.CreateDirectory(crdtProjectsFolder);
var crdtProject = await _services.ServiceProvider.GetRequiredService<ProjectsService>()
.CreateProject(new(_projectName, projectGuid));
_services.ServiceProvider.GetRequiredService<ProjectContext>().Project = crdtProject;
CrdtApi = _services.ServiceProvider.GetRequiredService<IMiniLcmApi>();
.CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId));
CrdtApi = (CrdtMiniLcmApi) await _services.ServiceProvider.OpenCrdtProject(crdtProject);
}

public async Task DisposeAsync()
{
await _services.DisposeAsync();
}

public IMiniLcmApi CrdtApi { get; set; } = null!;
public CrdtMiniLcmApi CrdtApi { get; set; } = null!;
public FwDataMiniLcmApi FwDataApi { get; set; } = null!;
}

public class MockProjectContext(CrdtProject project) : ProjectContext
public class MockProjectContext(CrdtProject? project) : ProjectContext
{
public override CrdtProject? Project { get; set; } = project;
}
2 changes: 1 addition & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/SyncFixtureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class SyncFixtureTests
[Fact]
public async Task CanStart()
{
var fixture = SyncFixture.Create("test-sync-fixture");
var fixture = SyncFixture.Create();
await fixture.InitializeAsync();
await fixture.DisposeAsync();
}
Expand Down
22 changes: 22 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using FwLiteProjectSync.Tests.Fixtures;
using LcmCrdt;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MiniLcm;
using MiniLcm.Models;
using SystemTextJsonPatch;
Expand Down Expand Up @@ -83,6 +86,25 @@ public async Task FirstSyncJustDoesAnImport()
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

[Fact]
public static async Task SyncFailsWithMismatchedProjectIds()
{
var fixture = SyncFixture.Create();
await fixture.InitializeAsync();
var crdtApi = fixture.CrdtApi;
var fwdataApi = fixture.FwDataApi;
await fixture.SyncService.Sync(crdtApi, fwdataApi);

var newFwProjectId = Guid.NewGuid();
await fixture.Services.GetRequiredService<LcmCrdtDbContext>().ProjectData.
ExecuteUpdateAsync(updates => updates.SetProperty(p => p.FwProjectId, newFwProjectId));
await fixture.Services.GetRequiredService<CurrentProjectService>().PopulateProjectDataCache(force: true);

Func<Task> syncTask = async () => await fixture.SyncService.Sync(crdtApi, fwdataApi);
await syncTask.Should().ThrowAsync<InvalidOperationException>();
await fixture.DisposeAsync();
}

[Fact]
public async Task CreatingAnEntryInEachProjectSyncsAcrossBoth()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public record SyncResult(int CrdtChanges, int FwdataChanges);

public async Task<SyncResult> Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataApi, bool dryRun = false)
{
if (crdtApi is CrdtMiniLcmApi crdt && crdt.ProjectData.FwProjectId != fwdataApi.ProjectId)
{
throw new InvalidOperationException($"Project id mismatch, CRDT Id: {crdt.ProjectData.FwProjectId}, FWData Id: {fwdataApi.ProjectId}");
}
var projectSnapshot = await GetProjectSnapshot(fwdataApi.Project.Name, fwdataApi.Project.ProjectsPath);
SyncResult result = await Sync(crdtApi, fwdataApi, dryRun, fwdataApi.EntryCount, projectSnapshot);

Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/FwLiteProjectSync/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static Task<int> Main(string[] args)
var crdtProject = projectsService.GetProject(crdtProjectName);
if (crdtProject is null)
{
crdtProject = await projectsService.CreateProject(new(crdtProjectName, fwdataApi.ProjectId, SeedNewProjectData: false));
crdtProject = await projectsService.CreateProject(new(crdtProjectName, FwProjectId:fwdataApi.ProjectId, SeedNewProjectData: false));
}
var syncService = services.GetRequiredService<CrdtFwdataProjectSyncService>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Properties:
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
ClientId (Guid) Required
FwProjectId (Guid?)
Name (string) Required
OriginDomain (string)
Keys:
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace LcmCrdt;
public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectService) : IMiniLcmApi
{
private Guid ClientId { get; } = projectService.ProjectData.ClientId;

public ProjectData ProjectData => projectService.ProjectData;

private IQueryable<Entry> Entries => dataModel.GetLatestObjects<Entry>();
private IQueryable<ComplexFormComponent> ComplexFormComponents => dataModel.GetLatestObjects<ComplexFormComponent>();
Expand Down
10 changes: 9 additions & 1 deletion backend/FwLite/LcmCrdt/CrdtProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ public class CrdtProject(string name, string dbPath) : IProjectIdentifier
public ProjectData? Data { get; set; }
}

public record ProjectData(string Name, Guid Id, string? OriginDomain, Guid ClientId)
/// <summary>
///
/// </summary>
/// <param name="Name">Name of the project</param>
/// <param name="Id">Id, consistent across all clients, matches the project Id in Lexbox</param>
/// <param name="OriginDomain">Server to sync with, null if not synced</param>
/// <param name="ClientId">Unique id for this client machine</param>
/// <param name="FwProjectId">FieldWorks project id, aka LangProjectId</param>
public record ProjectData(string Name, Guid Id, string? OriginDomain, Guid ClientId, Guid? FwProjectId = null)
{
public static string? GetOriginDomain(Uri? uri)
{
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/LcmCrdt/CurrentProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ private static string CacheKey(Guid projectId)
return memoryCache.Get<ProjectData>(CacheKey(projectId));
}

public async ValueTask<ProjectData> PopulateProjectDataCache()
public async ValueTask<ProjectData> PopulateProjectDataCache(bool force = false)
{
if (force) RemoveProjectDataCache();
var projectData = await GetProjectData();
return projectData;
}
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/LcmCrdt/ProjectsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public record CreateProjectRequest(
Uri? Domain = null,
Func<IServiceProvider, CrdtProject, Task>? AfterCreate = null,
bool SeedNewProjectData = true,
string? Path = null);
string? Path = null,
Guid? FwProjectId = null);

public async Task<CrdtProject> CreateProject(CreateProjectRequest request)
{
Expand All @@ -55,7 +56,7 @@ public async Task<CrdtProject> CreateProject(CreateProjectRequest request)
var projectData = new ProjectData(name,
request.Id ?? Guid.NewGuid(),
ProjectData.GetOriginDomain(request.Domain),
Guid.NewGuid());
Guid.NewGuid(), request.FwProjectId);
await InitProjectDb(db, projectData);
await serviceScope.ServiceProvider.GetRequiredService<CurrentProjectService>().PopulateProjectDataCache();
if (request.SeedNewProjectData)
Expand Down
4 changes: 2 additions & 2 deletions backend/FwLite/LocalWebApp/Services/ImportFwdataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ public async Task<CrdtProject> Import(string projectName)
}
try
{
using var fwDataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, false);
var project = await projectsService.CreateProject(new(fwDataProject.Name,
SeedNewProjectData: false,
FwProjectId: fwDataApi.ProjectId,
AfterCreate: async (provider, project) =>
{
using var fwDataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, false);
var crdtApi = provider.GetRequiredService<IMiniLcmApi>();
await miniLcmImport.ImportProject(crdtApi, fwDataApi, fwDataApi.EntryCount);
}));
var timeSpent = Stopwatch.GetElapsedTime(startTime);
logger.LogInformation("Import of {ProjectName} complete, took {TimeSpend}", fwDataProject.Name, timeSpent.Humanize(2));
return project;

}
catch
{
Expand Down

0 comments on commit f72f3e0

Please sign in to comment.