diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index d717033990..ac05ebb181 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -109,9 +109,9 @@ - - - + + + diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt index b28168b1ba..b66623ab55 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt @@ -943,17 +943,37 @@ { "$type": "uploaded:RemoteResource", "RemoteId": "withdrawal", + "Metadata": null, "EntityId": "9c8b252c-d342-dfe8-c1dd-5899471ecbd1" }, + { + "$type": "uploaded:RemoteResource", + "RemoteId": "Identity", + "Metadata": {}, + "EntityId": "c0fe5fa7-76ba-de0c-2a94-316fbbb84b0c" + }, { "$type": "create:remote-resource", "RemoteId": "Buckinghamshire", + "Metadata": null, "EntityId": "188a57e7-7d61-8742-59eb-c0bfbf959737" }, + { + "$type": "create:remote-resource", + "RemoteId": "back-end", + "Metadata": {}, + "EntityId": "0aace990-edd6-295c-2538-7611f0622e6e" + }, { "$type": "create:pendingUpload", + "Metadata": null, "EntityId": "52d7432c-ad4f-430c-8304-925603b3fefa" }, + { + "$type": "create:pendingUpload", + "Metadata": {}, + "EntityId": "f58a4713-11d6-999e-d355-0344e4fcd7b3" + }, { "$type": "delete:RemoteResource", "EntityId": "e14de071-3ed0-b12a-7865-da43fbfe8f84" @@ -1293,5 +1313,10 @@ { "$type": "SetMainPublicationChange", "EntityId": "c775e310-fc5c-6d01-5a02-146d579a88e5" + }, + { + "$type": "set:remote-resource-metadata", + "Metadata": {}, + "EntityId": "ef64afaa-972e-34b2-b57f-ae99e543b5ea" } ] \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt index 19edc4c876..ce4bde917a 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt @@ -625,5 +625,42 @@ ], "EntityId": "f27a032b-e0f6-f389-b466-959bdb17086d" } + }, + { + "Input": { + "$type": "uploaded:RemoteResource", + "RemoteId": "withdrawal", + "EntityId": "9c8b252c-d342-dfe8-c1dd-5899471ecbd1" + }, + "Output": { + "$type": "uploaded:RemoteResource", + "RemoteId": "withdrawal", + "Metadata": null, + "EntityId": "9c8b252c-d342-dfe8-c1dd-5899471ecbd1" + } + }, + { + "Input": { + "$type": "create:remote-resource", + "RemoteId": "Buckinghamshire", + "EntityId": "188a57e7-7d61-8742-59eb-c0bfbf959737" + }, + "Output": { + "$type": "create:remote-resource", + "RemoteId": "Buckinghamshire", + "Metadata": null, + "EntityId": "188a57e7-7d61-8742-59eb-c0bfbf959737" + } + }, + { + "Input": { + "$type": "create:pendingUpload", + "EntityId": "52d7432c-ad4f-430c-8304-925603b3fefa" + }, + "Output": { + "$type": "create:pendingUpload", + "Metadata": null, + "EntityId": "52d7432c-ad4f-430c-8304-925603b3fefa" + } } ] \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs index 7b81d89da0..3f63313136 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs @@ -280,12 +280,19 @@ private static IEnumerable GetAllChanges() var removePublicationChange = new RemovePublicationChange(entry.Id, publication2.Id); yield return new ChangeWithDependencies(removePublicationChange, [replacePublicationChange]); - yield return new ChangeWithDependencies(new CreateRemoteResourceChange(Guid.NewGuid(), "test-remote-id")); - var createRemoteResourcePendingUploadChange = new CreateRemoteResourcePendingUploadChange(Guid.NewGuid()); + var createRemoteResourceChange = new CreateRemoteResourceChange(Guid.NewGuid(), "test-remote-id"); + yield return new ChangeWithDependencies(createRemoteResourceChange); + var createRemoteResourcePendingUploadChange = new CreateRemoteResourcePendingUploadChange(Guid.NewGuid()); yield return new ChangeWithDependencies(createRemoteResourcePendingUploadChange); yield return new ChangeWithDependencies( - new RemoteResourceUploadedChange(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id"), + new RemoteResourceUploadedChange(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id"), [createRemoteResourcePendingUploadChange]); + yield return new ChangeWithDependencies( + new SetRemoteResourceMetadataChange(createRemoteResourceChange.EntityId, new NoMetadata()), + [createRemoteResourceChange]); + yield return new ChangeWithDependencies( + new DeleteRemoteResourceChange(createRemoteResourceChange.EntityId), + [createRemoteResourceChange]); var customView = new CustomView { diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index de8092deca..b427db3e76 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -12,10 +12,11 @@ public class ConfigRegistrationTests private readonly HashSet _excludedChangeTypes = [ typeof(JsonPatchChange), //not supported - typeof(JsonPatchChange), //not supported + typeof(JsonPatchChange>), //not supported typeof(JsonPatchChange), //replaced by JsonPatchExampleSentenceChange typeof(JsonPatchChange), //not supported. Use EditCustomViewChange typeof(DeleteChange), //MorphTypes cannot be deleted + typeof(DeleteChange>), //replaced by DeleteRemoteResourceChange ]; private readonly CrdtConfig _config; diff --git a/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt b/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt index b7e45c5eb4..92a12b476d 100644 --- a/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt @@ -3828,7 +3828,15 @@ "$type": "RemoteResource", "Id": "fce4cb31-93bd-f380-fd73-3fb0d09c0fa5", "DeletedAt": "2025-09-17T23:53:03.6147633+02:00", - "RemoteId": "end-to-end" + "RemoteId": "end-to-end", + "Metadata": null + }, + { + "$type": "RemoteResource", + "Id": "666023e2-5d9e-58e1-15ba-d47562be1752", + "DeletedAt": "2026-07-01T14:13:37.7894421+02:00", + "RemoteId": "4th generation", + "Metadata": {} }, { "$type": "MiniLcmCrdtAdapter", diff --git a/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt b/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt index a3fe21d28d..a55b3e4dac 100644 --- a/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt @@ -3900,5 +3900,20 @@ "Id": "853b91e6-d738-7d5d-cc98-e62904160c35", "DeletedAt": null } + }, + { + "Input": { + "$type": "RemoteResource", + "Id": "fce4cb31-93bd-f380-fd73-3fb0d09c0fa5", + "DeletedAt": "2025-09-17T23:53:03.6147633+02:00", + "RemoteId": "end-to-end" + }, + "Output": { + "$type": "RemoteResource", + "Id": "fce4cb31-93bd-f380-fd73-3fb0d09c0fa5", + "DeletedAt": "2025-09-17T23:53:03.6147633+02:00", + "RemoteId": "end-to-end", + "Metadata": null + } } ] \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt index f11904f409..adbfb372fe 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt @@ -1,19 +1,23 @@ { DerivedTypes: [ { - DerivedType: RemoteResourceUploadedChange, + DerivedType: RemoteResourceUploadedChange, TypeDiscriminator: uploaded:RemoteResource }, { - DerivedType: CreateRemoteResourceChange, + DerivedType: CreateRemoteResourceChange, TypeDiscriminator: create:remote-resource }, { - DerivedType: CreateRemoteResourcePendingUploadChange, + DerivedType: CreateRemoteResourcePendingUploadChange, TypeDiscriminator: create:pendingUpload }, { - DerivedType: DeleteChange, + DerivedType: SetRemoteResourceMetadataChange, + TypeDiscriminator: set:remote-resource-metadata + }, + { + DerivedType: DeleteRemoteResourceChange, TypeDiscriminator: delete:RemoteResource }, { diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt index 6eaccd9213..3b4baa1e40 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -466,16 +466,19 @@ Relational:TableName: LocalResource Relational:ViewName: Relational:ViewSchema: - EntityType: RemoteResource + EntityType: RemoteResource Properties: Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd DeletedAt (DateTimeOffset?) + Metadata (NoMetadata) + Annotations: + Relational:ColumnType: jsonb RemoteId (string) SnapshotId (no field, Guid?) Shadow FK Index Keys: Id PK Foreign keys: - RemoteResource {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + RemoteResource {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull Indexes: SnapshotId Unique Annotations: diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt index d2269bf97a..2742eb0b30 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt @@ -5,7 +5,7 @@ TypeDiscriminator: MiniLcmCrdtAdapter }, { - DerivedType: RemoteResource, + DerivedType: RemoteResource, TypeDiscriminator: RemoteResource } ], diff --git a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs index 0548cb0c98..7de4ec6729 100644 --- a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs @@ -16,7 +16,7 @@ public static IEnumerable GetEntityTypes() var crdtConfig = new CrdtConfig(); LcmCrdtKernel.ConfigureCrdt(crdtConfig); return crdtConfig.ObjectTypes - .Except([typeof(RemoteResource)])//exclude remote resource as it's a harmony defined type, not miniLcm + .Except([typeof(RemoteResource)])//exclude remote resource as it's a harmony defined type, not miniLcm .Select(t => new object[] { t }); } diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 4aaa97f3b7..b2a6699e39 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -4,6 +4,7 @@ using SIL.Harmony.Linq2db; using SIL.Harmony.Core; using SIL.Harmony.Changes; +using SIL.Harmony.Resource; using LcmCrdt.Changes; using LcmCrdt.Changes.CustomJsonPatches; using LcmCrdt.Changes.Entries; @@ -61,6 +62,10 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se services.RemoveAll();//we don't want to be able to inject these directly as they will leak. services.AddOptions().BindConfiguration("LcmCrdt"); + // Order matters: AddCrdtRemoteResources' Configure delegate must run before + // ConfigureCrdt so the RemoteResourcesEnabled guard in ConfigureCrdt sees enabled=true and + // skips its own AddRemoteResourceEntity call. Configure delegates run in registration order. + services.AddCrdtRemoteResources(); services.AddCrdtDataDbFactory( ConfigureCrdt ); @@ -307,7 +312,7 @@ public static void ConfigureCrdt(CrdtConfig config) }).IsUnique().HasFilter($"{componentSenseId} IS NULL"); }); - config.AddRemoteResourceEntity(); + if (!config.RemoteResourcesEnabled) config.AddRemoteResourceEntity(); config.ChangeTypeListBuilder.Add>() .Add>() diff --git a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs index e5a8886967..5992518a38 100644 --- a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs +++ b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs @@ -11,15 +11,15 @@ namespace LcmCrdt.MediaServer; public class LcmMediaService( - ResourceService resourceService, + ResourceService resourceService, CurrentProjectService currentProjectService, IOptions options, IRefitHttpServiceFactory refitFactory, IServerHttpClientProvider httpClientProvider, ILogger logger -) : IRemoteResourceService +) : IRemoteResourceService { - public async Task AllResources() + public async Task[]> AllResources() { return await resourceService.AllResources(); } @@ -87,7 +87,7 @@ private async Task MediaServerClient() return mediaClient; } - async Task IRemoteResourceService.DownloadResource(string remoteId, string localResourceCachePath) + async Task IRemoteResourceService.DownloadResource(string remoteId, string localResourceCachePath) { var projectResourceCachePath = ProjectResourceCachePath; Directory.CreateDirectory(projectResourceCachePath); @@ -112,7 +112,7 @@ public static string ProjectCachePath(CrdtProject project, CrdtConfig options) } - async Task IRemoteResourceService.UploadResource(Guid resourceId, string localPath) + async Task> IRemoteResourceService.UploadResource(Guid resourceId, string localPath, NoMetadata? metadata) { var mediaClient = await MediaServerClient(); var fileName = Path.GetFileName(localPath); @@ -121,10 +121,10 @@ await mediaClient.UploadFile( projectId: currentProjectService.ProjectData.Id, fileId: resourceId.ToString("D"), filename: fileName); - return new UploadResult(resourceId.ToString("N")); + return new UploadResult(resourceId.ToString("N")); } - public async Task<(HarmonyResource resource, bool newResource)> SaveFile(Stream stream, LcmFileMetadata metadata) + public async Task<(HarmonyResource resource, bool newResource)> SaveFile(Stream stream, LcmFileMetadata metadata) { var projectResourceCachePath = ProjectResourceCachePath; Directory.CreateDirectory(projectResourceCachePath); @@ -138,7 +138,7 @@ await mediaClient.UploadFile( try { - IRemoteResourceService? remoteResourceService = null; + IRemoteResourceService? remoteResourceService = null; if (await httpClientProvider.ConnectionStatus() == ConnectionStatus.Online) remoteResourceService = this; return (await resourceService.AddLocalResource( localPath, diff --git a/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.Designer.cs b/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.Designer.cs new file mode 100644 index 0000000000..db63eeadec --- /dev/null +++ b/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.Designer.cs @@ -0,0 +1,855 @@ +// +using System; +using System.Collections.Generic; +using LcmCrdt; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LcmCrdt.Migrations +{ + [DbContext(typeof(LcmCrdtDbContext))] + [Migration("20260701154631_AddRemoteResourceMetadata")] + partial class AddRemoteResourceMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("LcmCrdt.FullTextSearch.EntrySearchRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CitationForm") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Gloss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LexemeForm") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EntrySearchRecord", null, t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("LcmCrdt.ProjectData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FwProjectId") + .HasColumnType("TEXT"); + + b.Property("LastUserId") + .HasColumnType("TEXT"); + + b.Property("LastUserName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginDomain") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Editor"); + + b.HasKey("Id"); + + b.ToTable("ProjectData"); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ComplexFormEntryId") + .HasColumnType("TEXT"); + + b.Property("ComplexFormHeadword") + .HasColumnType("TEXT"); + + b.Property("ComponentEntryId") + .HasColumnType("TEXT"); + + b.Property("ComponentHeadword") + .HasColumnType("TEXT"); + + b.Property("ComponentSenseId") + .HasColumnType("TEXT") + .HasColumnName("ComponentSenseId"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ComponentEntryId"); + + b.HasIndex("ComponentSenseId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.HasIndex("ComplexFormEntryId", "ComponentEntryId") + .IsUnique() + .HasFilter("ComponentSenseId IS NULL"); + + b.HasIndex("ComplexFormEntryId", "ComponentEntryId", "ComponentSenseId") + .IsUnique() + .HasFilter("ComponentSenseId IS NOT NULL"); + + b.ToTable("ComplexFormComponents", (string)null); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("ComplexFormType"); + }); + + modelBuilder.Entity("MiniLcm.Models.CustomView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Analysis") + .HasColumnType("jsonb"); + + b.Property("Base") + .HasColumnType("INTEGER"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("EntryFields") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ExampleFields") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SenseFields") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.Property("Vernacular") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("CustomView"); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CitationForm") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ComplexFormTypes") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("HomographNumber") + .HasColumnType("INTEGER"); + + b.Property("LexemeForm") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("LiteralMeaning") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MorphType") + .HasColumnType("INTEGER"); + + b.Property("Note") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PublishIn") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Entry"); + }); + + modelBuilder.Entity("MiniLcm.Models.ExampleSentence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("Reference") + .HasColumnType("jsonb"); + + b.Property("SenseId") + .HasColumnType("TEXT"); + + b.Property("Sentence") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.Property("Translations") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SenseId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("ExampleSentence"); + }); + + modelBuilder.Entity("MiniLcm.Models.MorphType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Abbreviation") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Postfix") + .HasColumnType("TEXT"); + + b.Property("Prefix") + .HasColumnType("TEXT"); + + b.Property("SecondaryOrder") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Kind") + .IsUnique(); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("MorphType"); + }); + + modelBuilder.Entity("MiniLcm.Models.PartOfSpeech", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Predefined") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("PartOfSpeech"); + }); + + modelBuilder.Entity("MiniLcm.Models.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsMain") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Publication"); + }); + + modelBuilder.Entity("MiniLcm.Models.SemanticDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Predefined") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("SemanticDomain"); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("Gloss") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("PartOfSpeechId") + .HasColumnType("TEXT"); + + b.Property("Pictures") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'[]'"); + + b.Property("SemanticDomains") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntryId"); + + b.HasIndex("PartOfSpeechId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Sense"); + }); + + modelBuilder.Entity("MiniLcm.Models.WritingSystem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Abbreviation") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Exemplars") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Font") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WsId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.HasIndex("WsId", "Type") + .IsUnique(); + + b.ToTable("WritingSystem"); + }); + + modelBuilder.Entity("SIL.Harmony.Commit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ParentHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.ComplexProperty(typeof(Dictionary), "HybridDateTime", "SIL.Harmony.Commit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("INTEGER") + .HasColumnName("Counter"); + + b1.Property("DateTime") + .HasColumnType("TEXT") + .HasColumnName("DateTime"); + }); + + b.HasKey("Id"); + + b.ToTable("Commits", (string)null); + + b.HasAnnotation("CustomIndex:CompositeIndexes", "[{\"paths\":[\"HybridDateTime.DateTime\",\"HybridDateTime.Counter\",\"Id\"],\"unique\":false,\"name\":\"IX_Commits_DateTime_Counter_Id\"}]"); + }); + + modelBuilder.Entity("SIL.Harmony.Core.ChangeEntity", b => + { + b.Property("CommitId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Change") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.HasKey("CommitId", "Index"); + + b.ToTable("ChangeEntities", (string)null); + }); + + modelBuilder.Entity("SIL.Harmony.Db.ObjectSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommitId") + .HasColumnType("TEXT"); + + b.Property("Entity") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityIsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRoot") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("References") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntityId"); + + b.HasIndex("CommitId", "EntityId") + .IsUnique(); + + b.ToTable("Snapshots", (string)null); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.LocalResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LocalResource"); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("RemoteResource", (string)null); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b => + { + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("Components") + .HasForeignKey("ComplexFormEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("ComplexForms") + .HasForeignKey("ComponentEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.Sense", null) + .WithMany() + .HasForeignKey("ComponentSenseId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ComplexFormComponent", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormType", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ComplexFormType", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.CustomView", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.CustomView", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Entry", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.ExampleSentence", b => + { + b.HasOne("MiniLcm.Models.Sense", null) + .WithMany("ExampleSentences") + .HasForeignKey("SenseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ExampleSentence", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.MorphType", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.MorphType", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.PartOfSpeech", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.PartOfSpeech", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Publication", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Publication", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.SemanticDomain", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.SemanticDomain", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("Senses") + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.PartOfSpeech", "PartOfSpeech") + .WithMany() + .HasForeignKey("PartOfSpeechId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Sense", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PartOfSpeech"); + }); + + modelBuilder.Entity("MiniLcm.Models.WritingSystem", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.WritingSystem", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("SIL.Harmony.Core.ChangeEntity", b => + { + b.HasOne("SIL.Harmony.Commit", null) + .WithMany("ChangeEntities") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SIL.Harmony.Db.ObjectSnapshot", b => + { + b.HasOne("SIL.Harmony.Commit", "Commit") + .WithMany("Snapshots") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Commit"); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("SIL.Harmony.Resource.RemoteResource", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.Navigation("ComplexForms"); + + b.Navigation("Components"); + + b.Navigation("Senses"); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.Navigation("ExampleSentences"); + }); + + modelBuilder.Entity("SIL.Harmony.Commit", b => + { + b.Navigation("ChangeEntities"); + + b.Navigation("Snapshots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.cs b/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.cs new file mode 100644 index 0000000000..a3efcd5f39 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Migrations/20260701154631_AddRemoteResourceMetadata.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LcmCrdt.Migrations +{ + /// + public partial class AddRemoteResourceMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Metadata", + table: "RemoteResource", + type: "jsonb", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Metadata", + table: "RemoteResource"); + } + } +} diff --git a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs index 650a5b9420..088f5188e0 100644 --- a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs +++ b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs @@ -648,7 +648,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("LocalResource"); }); - modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -657,6 +657,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DeletedAt") .HasColumnType("TEXT"); + b.Property("Metadata") + .HasColumnType("jsonb"); + b.Property("RemoteId") .HasColumnType("TEXT"); @@ -668,7 +671,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SnapshotId") .IsUnique(); - b.ToTable("RemoteResource"); + b.ToTable("RemoteResource", (string)null); }); modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b => @@ -815,11 +818,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Commit"); }); - modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => { b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) .WithOne() - .HasForeignKey("SIL.Harmony.Resource.RemoteResource", "SnapshotId") + .HasForeignKey("SIL.Harmony.Resource.RemoteResource", "SnapshotId") .OnDelete(DeleteBehavior.SetNull); }); diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IRemoteResource.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IRemoteResource.ts index be91b9ed50..6a8f3ca777 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IRemoteResource.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IRemoteResource.ts @@ -8,5 +8,6 @@ export interface IRemoteResource id: string; deletedAt?: string; remoteId?: string; + metadata?: unknown; } /* eslint-enable */